Skip to content

Commit 4d70f3e

Browse files
committed
Refactoring random strategy
1 parent 9b586c4 commit 4d70f3e

6 files changed

Lines changed: 110 additions & 64 deletions

File tree

include/models/trade.hpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ struct Trade {
2626
std::string dealReference;
2727
std::string symbol;
2828
int scalingFactor;
29-
double stopDistancePips;
30-
double limitDistancePips;
29+
boost::decimal::decimal64_t stopDistancePips;
30+
boost::decimal::decimal64_t limitDistancePips;
3131
std::string strategyId;
3232
std::string strategyName;
3333

include/strategies/randomStrategy.hpp

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@
1616
class TradeManager; // forward declared in strategy.hpp; redeclaring is harmless
1717

1818
// A trivial strategy: on every tick it flips a fair coin and returns
19-
// LONG or SHORT. While trades are open it has a small per-tick chance
20-
// of closing every active position. Intended as scaffolding for the
21-
// strategy interface, not as a real trading approach.
19+
// LONG or SHORT. Exits are not the strategy's responsibility — they
20+
// are driven by the stop-loss / take-profit pip distances configured
21+
// on the trade and enforced centrally by Operations. Intended as
22+
// scaffolding for the strategy interface, not as a real trading
23+
// approach.
2224
//
2325
// C# parallels for readers from a C# background:
2426
// - `class` here is a value/owning type managed via stack or
@@ -49,11 +51,12 @@ class RandomStrategy : public IStrategy {
4951
// interface stable for strategies that will look at price.
5052
std::optional<Direction> decide(const PriceData& tick) override;
5153

52-
// Per-tick management hook. With a small probability per tick
53-
// (see `closeProb`), closes every currently-open trade at the
54-
// tick's bid price. `tradeManager` is passed by mutable reference
55-
// because the strategy needs to call mutating methods on it
56-
// (`closeTrade`); `getActiveTrades()` is still safely const.
54+
// Per-tick management hook. The default RandomStrategy
55+
// implementation is a no-op — Operations closes trades when
56+
// their stop-loss or take-profit boundary is hit. `tradeManager`
57+
// is passed by mutable reference so future strategies (trailing
58+
// stops, partial closes, scale-ins) can act on open positions
59+
// here without changing the interface.
5760
void during(std::size_t tickValue,
5861
const PriceData& price,
5962
TradeManager& tradeManager) override;

include/trading/tradeManager.hpp

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,11 @@ class TradeManager {
1919

2020
public:
2121
TradeManager() = default;
22-
std::string openTrade(const PriceData& tick, boost::decimal::decimal64_t size, Direction direction);
22+
std::string openTrade(const PriceData& tick,
23+
boost::decimal::decimal64_t size,
24+
Direction direction,
25+
boost::decimal::decimal64_t stopDistancePips = boost::decimal::decimal64_t{0},
26+
boost::decimal::decimal64_t limitDistancePips = boost::decimal::decimal64_t{0});
2327
size_t reviewAccount() const;
2428
bool closeTrade(const std::string& tradeId, boost::decimal::decimal64_t closePrice);
2529
const std::unordered_map<std::string, Trade>& getActiveTrades() const;

source/operations.cpp

Lines changed: 78 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -9,62 +9,113 @@
99
#include <iostream>
1010
#include <vector>
1111
#include <memory>
12+
#include <optional>
1213
#include <string>
14+
#include <utility>
1315
#include <iomanip>
1416
#include <cstdio>
1517
#include <ctime>
1618
#include <boost/decimal.hpp>
1719
#include "tradeManager.hpp"
20+
#include "models/symbolScale.hpp"
1821
#include "strategies/randomStrategy.hpp"
1922

23+
namespace {
24+
25+
// Decide whether the current tick has hit a trade's stop-loss or
26+
// take-profit boundary. Returns the price at which the position would
27+
// close (bid for LONG exits, ask for SHORT exits) along with a flag
28+
// — `std::nullopt` means "no exit on this tick".
29+
//
30+
// Pip → price conversion uses the symbol's scaling factor: a 1.5-pip
31+
// distance on EURUSD (scale 10000) is 0.00015; on USDJPY (scale 100)
32+
// it's 0.015. Trades on unknown symbols (scale 0) are skipped — there
33+
// is no sensible pip distance to apply.
34+
std::optional<boost::decimal::decimal64_t>
35+
checkExit(const Trade& trade, const PriceData& tick) {
36+
if (trade.scalingFactor == 0) return std::nullopt;
37+
if (trade.stopDistancePips == 0 &&
38+
trade.limitDistancePips == 0) {
39+
return std::nullopt;
40+
}
41+
42+
const auto stopOffset = trade.stopDistancePips / trade.scalingFactor;
43+
const auto limitOffset = trade.limitDistancePips / trade.scalingFactor;
44+
45+
if (trade.direction == Direction::LONG) {
46+
const auto stopPrice = trade.entryPrice - stopOffset;
47+
const auto limitPrice = trade.entryPrice + limitOffset;
48+
// Exit a long at the bid (the price the broker pays us).
49+
if (trade.stopDistancePips != 0 && tick.bid <= stopPrice) return tick.bid;
50+
if (trade.limitDistancePips != 0 && tick.bid >= limitPrice) return tick.bid;
51+
} else {
52+
const auto stopPrice = trade.entryPrice + stopOffset;
53+
const auto limitPrice = trade.entryPrice - limitOffset;
54+
// Exit a short at the ask (the price we pay to buy back).
55+
if (trade.stopDistancePips != 0 && tick.ask >= stopPrice) return tick.ask;
56+
if (trade.limitDistancePips != 0 && tick.ask <= limitPrice) return tick.ask;
57+
}
58+
return std::nullopt;
59+
}
60+
61+
// Walk every active trade, close any whose SL/TP has been hit on this
62+
// tick. Two-phase to avoid invalidating the map iterator while erasing.
63+
void reviewStopAndLimit(TradeManager& tradeManager, const PriceData& tick) {
64+
const auto& openTrades = tradeManager.getActiveTrades();
65+
if (openTrades.empty()) return;
66+
67+
std::vector<std::pair<std::string, boost::decimal::decimal64_t>> toClose;
68+
toClose.reserve(openTrades.size());
69+
for (const auto& [id, trade] : openTrades) {
70+
if (auto exitPrice = checkExit(trade, tick)) {
71+
toClose.emplace_back(id, *exitPrice);
72+
}
73+
}
74+
for (const auto& [id, exitPrice] : toClose) {
75+
tradeManager.closeTrade(id, exitPrice);
76+
}
77+
}
78+
79+
} // namespace
80+
2081
void Operations::run(const std::vector<PriceData>& ticks,
2182
const trading_definitions::Configuration& config) {
2283

23-
// Create
24-
auto tradeManager = new TradeManager();
84+
auto tradeManager = std::make_unique<TradeManager>();
2585
RandomStrategy strategy(config.STRATEGY);
2686

87+
const auto& tradingVars = config.STRATEGY.TRADING_VARIABLES;
88+
2789
std::size_t tickIndex = 0;
2890
for (const auto& tick : ticks) {
2991

92+
// Close any trade whose stop-loss or take-profit fired on this tick
93+
// before we consider opening a new one — otherwise an exit and an
94+
// entry could race within the same tick.
95+
reviewStopAndLimit(*tradeManager, tick);
96+
3097
size_t openTrades = tradeManager->reviewAccount();
3198

3299
// only open a trade if there is zero
33100
if (openTrades == 0) {
34101
// optional is false
35102
if (auto signal = strategy.decide(tick)) {
36-
std::string tradeId = tradeManager->openTrade(tick, config.STRATEGY.TRADING_VARIABLES.TRADING_SIZE, *signal);
37-
std::cout << "Opened trade: " << tradeId << std::endl;
103+
tradeManager->openTrade(tick,
104+
tradingVars.TRADING_SIZE,
105+
*signal,
106+
tradingVars.STOP_DISTANCE_IN_PIPS,
107+
tradingVars.LIMIT_DISTANCE_IN_PIPS);
38108
}
39109
}
40110

41-
// this would be a position manager review point
42-
// randomly check account status every 100 ticks
43-
if (openTrades > 0 && (std::rand() % 100) == 0) { // NOSONAR(cpp:S2245) experimentation only, not security-sensitive
44-
std::cout << "Reviewing account at tick timestamp: " << tick.timestamp.time_since_epoch().count() << std::endl;
45-
std::cout << "Number of open trades: " << openTrades << std::endl;
46-
for (const auto& [id, trade] : tradeManager->getActiveTrades()) {
47-
std::cout << "Trade ID: " << id
48-
<< " | Entry: " << trade.entryPrice
49-
<< " | Size: " << trade.size
50-
<< " | Direction: " << (trade.direction == Direction::LONG ? "LONG" : "SHORT")
51-
<< std::endl;
52-
}
53-
}
54-
55-
// Strategy-driven management hook. The strategy itself decides
56-
// whether/when to close positions; the random strategy currently
57-
// closes every open trade with a small per-tick probability.
58-
// Dereferencing `tradeManager` (a raw pointer) yields a reference
59-
// — the parameter type is `TradeManager&`, so `*tradeManager`
60-
// is what we hand in.
111+
// Strategy-driven management hook for non-SL/TP exit logic
112+
// (e.g. trailing stops, partial closes). The default
113+
// RandomStrategy implementation is a no-op now that exits are
114+
// handled by reviewStopAndLimit above.
61115
strategy.during(tickIndex, tick, *tradeManager);
62116

63117
++tickIndex;
64118
}
65119

66120
std::cout << "Final PnL: " << std::fixed << std::setprecision(2) << tradeManager->calculatePnl() << std::endl;
67-
68-
// bool closed = tradeManager->closeTrade(tradeId);
69-
// std::cout << "Trade closed: " << (closed ? "yes" : "no") << std::endl;
70121
}

source/strategies/randomStrategy.cpp

Lines changed: 7 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,6 @@
66

77
#include "strategies/randomStrategy.hpp"
88

9-
#include <iostream>
10-
#include <string>
11-
#include <vector>
12-
139
#include "tradeManager.hpp"
1410

1511
// Member initialiser list (the bit after the `:`) runs in declaration
@@ -21,31 +17,17 @@ RandomStrategy::RandomStrategy(const trading_definitions::Strategy& strategyConf
2117
: config(strategyConfig),
2218
rng(std::random_device{}()), // seed once from the OS entropy source
2319
coin(0.5),
24-
closeProb(1.0 / 200.0) {} // ~0.5% chance per tick — same odds as the
25-
// old `std::rand() % 200 == 0` in operations.cpp
20+
closeProb(0.0) {} // unused — exits are driven by SL/TP in Operations
2621

2722
std::optional<Direction> RandomStrategy::decide(const PriceData& /*tick*/) {
2823
return coin(rng) ? Direction::LONG : Direction::SHORT;
2924
}
3025

3126
void RandomStrategy::during(std::size_t /*tickValue*/,
32-
const PriceData& price,
33-
TradeManager& tradeManager) {
34-
const auto& openTrades = tradeManager.getActiveTrades();
35-
if (openTrades.empty()) return;
36-
if (!closeProb(rng)) return;
37-
38-
// Two-phase pattern: collect IDs first, then close. Closing inside
39-
// the range-for loop would invalidate the iterator we're walking,
40-
// because `closeTrade` erases the entry from the underlying map.
41-
// Same trap as mutating a C# Dictionary while iterating it.
42-
std::vector<std::string> idsToClose;
43-
idsToClose.reserve(openTrades.size());
44-
for (const auto& [id, trade] : openTrades) {
45-
idsToClose.push_back(id);
46-
}
47-
for (const auto& id : idsToClose) {
48-
bool closed = tradeManager.closeTrade(id, price.bid);
49-
std::cout << "Closed trade ID: " << id << " - " << (closed ? "success" : "failure") << std::endl;
50-
}
27+
const PriceData& /*price*/,
28+
TradeManager& /*tradeManager*/) {
29+
// Exits are handled centrally by Operations using each trade's
30+
// stop-loss / take-profit pip distances. Strategies that want
31+
// bespoke exit logic (trailing stops, partial closes, etc.)
32+
// should override this hook.
5133
}

source/trading/tradeManager.cpp

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,16 @@ std::string nextTradeId() {
1414
}
1515
}
1616

17-
std::string TradeManager::openTrade(const PriceData& tick, boost::decimal::decimal64_t size, Direction direction) {
17+
std::string TradeManager::openTrade(const PriceData& tick,
18+
boost::decimal::decimal64_t size,
19+
Direction direction,
20+
boost::decimal::decimal64_t stopDistancePips,
21+
boost::decimal::decimal64_t limitDistancePips) {
1822
auto price = (direction == Direction::LONG) ? tick.ask : tick.bid;
1923
Trade trade(price, size, direction, tick.symbol);
2024
trade.id = nextTradeId();
25+
trade.stopDistancePips = stopDistancePips;
26+
trade.limitDistancePips = limitDistancePips;
2127
activeTrades[trade.id] = trade;
2228
return trade.id;
2329
}

0 commit comments

Comments
 (0)