algotradelib 0.0.0.9000
C++ header-only library for algorithmic trading
Loading...
Searching...
No Matches
example-inverse-fund.cpp
1// A bearish view on an index, held as an ordinary long position in an inverse fund.
2//
3// No adapter this library ships can sell what the account does not hold: KIS, Kiwoom and Toss take cash
4// orders. A target below zero there is refused by the guard before it is sent
5// (`RiskLimits::enforce_sellable_quantity_`, on by default). A replay can still simulate a short by
6// turning that check off where the replay is configured; that models no borrow, and nothing live.
7//
8// What a cash account can do is **buy a fund that moves against an index**. This example does exactly
9// that and nothing cleverer: the strategy reads one instrument for its signal and states a target for
10// another, the fund, by the fund's own identity. Nothing is translated, aliased or substituted behind
11// the strategy's back: the order, the fill, the holding and the exit are the fund's.
12//
13// What this is not, so the output is not read as more than it is:
14//
15// - It is not a short of the index, and it cannot stand in for a short of one stock. The fund targets
16// the opposite of its benchmark's **daily** return and resets every day, so over weeks its value
17// parts from what a short opened on day one would be worth, in either direction.
18// - It spends cash to buy fund shares. A short credits cash and owes shares, with a borrow fee, a
19// recall risk and margin. None of that is here.
20// - It carries the fund's own costs and tracking difference, and its benchmark is a futures index.
21// - Whether an account may buy a given fund (some need an agreement or a deposit) is the broker's to
22// say. This replay proves nothing about that.
23//
24// No credentials and no network. The two series below are a sample: the daily closes of 069500 and the
25// daily opens and closes of 114800 on the same forty trading days, 2026-08-11 to 2026-10-12, as a
26// public chart service reported them, unadjusted. They are here to make the example run, not as data
27// to rely on, and the replay stamps them one calendar day apart from an arbitrary start, not with
28// their real dates: replace them with your own history. A replay of this strategy must use the
29// fund's real bars: negating the index's returns to invent the fund's would hide exactly the
30// difference this file is warning about.
31//
32// Run it as:
33//
34// example-inverse-fund
35
36#include <algotradelib/backtest>
37
38#include <chrono>
39#include <cstddef>
40#include <iostream>
41#include <optional>
42#include <string>
43#include <vector>
44
45namespace {
46
48namespace backtest = algotradelib::backtest;
49
62
64InstrumentId index_fund() { return InstrumentId{Country::KR, Venue::COMBINED, "069500"}; }
65
67InstrumentId inverse_fund() { return InstrumentId{Country::KR, Venue::COMBINED, "114800"}; }
68
76struct InverseFundOnDowntrend {
78 InstrumentId signal_;
80 InstrumentId fund_;
82 std::size_t lookback_{5};
84 Decimal64 quantity_{Decimal64::parse("1000")};
85
86 std::vector<Decimal64> closes_;
87
88 template <class Ctx>
89 void on_bar(Ctx& ctx, const strategy::BarEvent& bar) {
90 // The fund's own bars are fed too, because its fills and its marks come from them. They carry
91 // no signal.
92 if (!(bar.instrument_ == signal_)) {
93 return;
94 }
95 closes_.push_back(bar.bar_.close_);
96 if (closes_.size() < lookback_) {
97 return;
98 }
99 // `close * n < sum` instead of `close < sum / n`: exact, and no scale to choose.
100 std::optional<Decimal64> sum = Decimal64::parse("0");
101 for (std::size_t at = closes_.size() - lookback_; sum && at < closes_.size(); ++at) {
102 sum = algotradelib::decimal_add(*sum, closes_[at]);
103 }
104 const auto scaled =
105 algotradelib::decimal_multiply(closes_.back(), Decimal64::parse(std::to_string(lookback_)));
106 if (!sum || !scaled) {
107 return;
108 }
109 const bool bearish = algotradelib::decimal_less(*scaled, *sum);
110
111 // A target for the fund, by the fund's name: never a negative target for the index.
112 strategy::TargetPosition target;
113 target.instrument_ = fund_;
114 target.target_quantity_ = bearish ? quantity_ : Decimal64::parse("0");
115 target.tag_ = "inverse-fund";
116 ctx.submit(target);
117 }
118};
119
121Instrument reference(const InstrumentId& id, const char* name) {
122 Instrument instrument;
123 instrument.id_ = id;
124 instrument.name_ = name;
125 instrument.type_ = SecurityType::ETF;
126 instrument.currency_ = Currency{"KRW"};
127 instrument.lot_size_ = Decimal64::parse("1");
128 // This example sends market orders and states no price. A fund has its own tick grid, which is
129 // not the equity ladder: take it from reference data before pricing an order for one.
130 instrument.price_rule_ = PriceRule::unknown("an exchange-traded fund's tick grid is not stated here");
131 return instrument;
132}
133
135struct Day {
136 int index_close_;
137 int fund_open_;
138 int fund_close_;
139};
140
142const std::vector<Day>& sample() {
143 static const std::vector<Day> days = {
144 {99265, 1147, 1126}, {103250, 1113, 1080}, {107270, 1043, 1039}, {110060, 1008, 1011},
145 {108550, 984, 1026}, {101760, 1079, 1089}, {108190, 1054, 1021}, {109980, 1035, 1003},
146 {105995, 1009, 1040}, {106795, 1070, 1032}, {107710, 1036, 1024}, {109135, 991, 1009},
147 {107180, 1021, 1028}, {107615, 1057, 1025}, {107885, 1034, 1021}, {103500, 1054, 1063},
148 {103610, 1047, 1061}, {105720, 1047, 1040}, {110820, 1004, 989}, {110335, 982, 994},
149 {112100, 992, 978}, {111865, 982, 980}, {109500, 1018, 1002}, {105410, 1038, 1038},
150 {104275, 1047, 1050}, {106365, 1050, 1029}, {106280, 1019, 1030}, {109285, 1003, 1000},
151 {111700, 994, 978}, {111880, 957, 978}, {113145, 958, 966}, {109500, 971, 998},
152 {109990, 1000, 993}, {109545, 986, 998}, {111520, 1006, 981}, {112060, 985, 975},
153 {110745, 971, 987}, {108550, 999, 1008}, {105650, 1009, 1035}, {102695, 1046, 1065},
154 };
155 return days;
156}
157
159Bar daily(std::chrono::system_clock::time_point at, int open, int close) {
160 Bar bar;
161 bar.timestamp_ = at;
162 const auto opened = Decimal64::parse(std::to_string(open));
163 const auto closed = Decimal64::parse(std::to_string(close));
164 bar.open_ = opened;
165 bar.close_ = closed;
166 bar.high_ = open > close ? opened : closed;
167 bar.low_ = open > close ? closed : opened;
168 bar.volume_ = Decimal64::parse("1000000");
169 return bar;
170}
171
172} // namespace
173
176
177int main() {
178 backtest::ReplayConfig config;
179 config.cash_ = Decimal64::parse("10000000");
180 config.instruments_ = {
181 reference(index_fund(), "an index fund (signal only)"),
182 reference(inverse_fund(), "an inverse fund (traded)"),
183 };
184 config.series_capacity_ = 64;
185 // Any phase, so the point stays the strategy: daily bars end outside the session.
186 config.limits_.permitted_phases_ = std::nullopt;
187 // The sellable check is left on, as it is by default. Every sell below is an exit of fund shares
188 // the replay holds, so nothing here needs it off; a strategy that wanted a true short would.
189
190 InverseFundOnDowntrend strategy_instance;
191 strategy_instance.signal_ = index_fund();
192 strategy_instance.fund_ = inverse_fund();
193
194 backtest::Backtester<InverseFundOnDowntrend> engine(std::move(strategy_instance), config);
195 const auto start = std::chrono::system_clock::time_point{} + std::chrono::hours(24 * 20000);
196 for (std::size_t at = 0; at < sample().size(); ++at) {
197 const auto when = start + std::chrono::hours(24 * static_cast<int>(at));
198 const auto& day = sample()[at];
199 // The fund's bar first: an order decided on yesterday's signal fills at this open. Then the
200 // signal's bar, on whose close the next decision is made.
201 engine.feed(strategy::make_bar_event(
202 inverse_fund(), BarInterval::ONE_DAY, BarStamp::CLOSE, daily(when, day.fund_open_, day.fund_close_)));
203 engine.feed(strategy::make_bar_event(
204 index_fund(), BarInterval::ONE_DAY, BarStamp::CLOSE, daily(when, day.index_close_, day.index_close_)));
205 }
206 engine.finish();
207
208 const auto& result = engine.result();
209 std::cout << "inverse fund 114800 traded on a signal from index fund 069500, " << sample().size()
210 << " sample days -- no credentials, no network\n\n";
211 for (const auto& execution : result.executions_) {
212 const auto& fill = execution.fill_;
213 const auto day = std::chrono::duration_cast<std::chrono::hours>(fill.at_ - start).count() / 24;
214 std::cout << " day " << day << " " << (fill.side_ == OrderSide::BUY ? "BUY " : "SELL") << " "
215 << fill.quantity_.str() << " fund shares at " << fill.price_.str() << "\n";
216 }
217 std::cout << "\n orders filled: " << result.executions_.size() << ", refused by the guard: "
218 << result.refused_.size() << ", not buildable: " << result.rejected_.size() << "\n";
219 if (!result.equity_.empty()) {
220 std::cout << " equity: " << config.cash_.str() << " -> " << result.equity_.back().equity_.str() << "\n";
221 }
222 std::cout
223 << "\nEvery order above is for the fund, and every sell is an exit of shares bought earlier: the\n"
224 << "account is never short, and the default sellable check never has anything to refuse.\n"
225 << "The fund follows the opposite of the index's move one day at a time. Held across many days its\n"
226 << "value is not what a short opened at the start would be worth, and it says nothing about any\n"
227 << "single stock. Forty sample days rank nothing.\n";
228 return 0;
229}
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
#define ATL_ASSERT_STRATEGY(S, Ctx)
Asserts at compile time that S is a strategy a runner with context Ctx can drive.
Definition concept.hpp:712
Replays a strategy over stored bars: fill and cost models, the replay ledger, and the report.
Definition clock.hpp:14
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 fill(S &strategy, Ctx &context, const Fill &event)
Calls strategy.on_fill(context, event) if the strategy declares on_fill; otherwise does nothing.
Definition concept.hpp:261
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
ProbeContextT< NoCaps > MinimalContext
A probe context whose broker implements nothing optional.
Definition concept.hpp:651
ProbeContextT< AllCaps > FullContext
A probe context whose broker implements everything optional.
Definition concept.hpp:654
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
Venue
A trading venue: an exchange, or the combined view across exchanges.
Definition types.hpp:43
bool decimal_less(const Decimal64 &lhs, const Decimal64 &rhs)
Compares two decimals numerically, whatever their scales.
Definition types.hpp:665
std::optional< Decimal64 > decimal_multiply(const Decimal64 &lhs, const Decimal64 &rhs)
Multiplies two decimals exactly; the scales add.
Definition types.hpp:571
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.
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.
InstrumentId instrument_
The instrument.
Definition intent.hpp:444
std::string tag_
A label carried to the orders and their fills.
Definition intent.hpp:450
Decimal64 target_quantity_
The position to hold, signed: negative is short, and zero means flat.
Definition intent.hpp:446