Transpiled version (13269L) is out of date.
1 | set flag Reparse. |
2 | replace real with double. |
3 | replace Real with Double. |
4 | replace price with float. |
5 | |
6 | // Persistence: |
7 | // MPM3.BotInAction is persistable which in turn means |
8 | // MPM3, Position, Juicer and other classes are also persistable. |
9 | // We don't persist Position.ticker for space reasons though. |
10 | |
11 | persistable sclass MPM3 { |
12 | // MARKET (trading platform) |
13 | |
14 | // How much percentual slippage we have for each position on |
15 | // average plus the fees for the position. |
16 | // Obviously dependent on the market (trading site) we are using. |
17 | settable real marketAdversity = 0.12 + 0.08; |
18 | |
19 | // SELF-IMPOSED LIMITS |
20 | |
21 | // How long a position can be held at the longest |
22 | // - only for backtesting! |
23 | gettable real maxPositionDuration = minutesToMS(60); |
24 | |
25 | real minimalMaxDuration() { ret minutesToMS(1); } |
26 | |
27 | // correct user if they are stupid |
28 | selfType maxPositionDuration(real whatUserWants) { |
29 | maxPositionDuration = max(whatUserWants, minimalMaxDuration()); |
30 | this; |
31 | } |
32 | |
33 | // POSITIONS |
34 | |
35 | // An open(ed) position |
36 | persistable record Position(transient TickerSequence ticker, real openingTime, real direction) { |
37 | |
38 | // Set this to a non-null value if this is a real position on a real market |
39 | settable O irlInfo; |
40 | |
41 | // Eye that opened this position |
42 | settable Eye eye; |
43 | |
44 | real openingPrice() { ret ticker.priceAtTimestamp(openingTime); } |
45 | |
46 | real profitAtTime(real time) { |
47 | ret (ticker.priceAtTimestamp(time)/openingPrice()*100-100)*direction - marketAdversity; |
48 | } |
49 | |
50 | real openingTime() { ret openingTime; } |
51 | |
52 | S directionString() { ret direction > 0 ? "long" : direction < 0 ? "short" : ""; } |
53 | |
54 | void restoreTicker(TickerSequence ticker) { this.ticker = ticker; } |
55 | |
56 | TickerPoint openingTickerPoint() { ret new TickerPoint(ticker, lround(openingTime)); } |
57 | } |
58 | |
59 | macro forwardToPosition { |
60 | TickerSequence ticker() { ret p.ticker; } |
61 | void restoreTicker(TickerSequence ticker) { p.restoreTicker(ticker); } |
62 | real direction() { ret p.direction; } |
63 | S directionString() { ret p.directionString(); } |
64 | real openingTime() { ret p.openingTime; } |
65 | real openingPrice() { ret p.openingPrice(); } |
66 | Eye eye() { ret p.eye(); } |
67 | } |
68 | |
69 | persistable record LivePosition(Position p) { |
70 | settable Juicer juicer; |
71 | |
72 | // highest profit this position has reached |
73 | settable real crest = -infinity(); |
74 | |
75 | settable real time; // time we last updated this position |
76 | settable real profit; // expected profit at that time |
77 | |
78 | forwardToPosition |
79 | |
80 | // return true if any change |
81 | bool update(real time) { |
82 | if (time <= this.time) false; |
83 | this.time = time; |
84 | profit = p.profitAtTime(time); |
85 | crest = max(crest, profit); |
86 | true; |
87 | } |
88 | |
89 | transient simplyCached real maxClosingTime() { |
90 | ret p.ticker.live() ? infinity() |
91 | : min(p.openingTime+maxPositionDuration, p.ticker.endTime()); |
92 | } |
93 | |
94 | bool shouldClose() { |
95 | ret time >= maxClosingTime() |
96 | || profit < (profit < 0 ? -juicer.lossTolerance : crest-juicer.pullback); |
97 | } |
98 | |
99 | // >= 1 to close |
100 | // otherwise keep open |
101 | real closeSignal() { |
102 | CloseSignal strongestSignal = closeSignalWithDescription(); |
103 | ret strongestSignal == null ? 0 : strongestSignal.trigger; |
104 | } |
105 | |
106 | CloseSignal closeSignalWithDescription() { |
107 | ret highestBy(closeSignals(), signal -> signal.trigger); |
108 | } |
109 | |
110 | // trigger = 0 to 1 |
111 | record noeq CloseSignal(S reason, real trigger) {} |
112 | |
113 | L<CloseSignal> closeSignals() { |
114 | new L<CloseSignal> signals; |
115 | |
116 | // How close are we to the max closing time? |
117 | if (maxClosingTime() < infinity()) |
118 | signals.add(new CloseSignal("Age", transformToZeroToOne(time, p.openingTime(), maxClosingTime()))); |
119 | |
120 | // How close are we to our loss limit? |
121 | if (profit < 0) |
122 | signals.add(new CloseSignal("Loss", doubleRatio(-profit, juicer.lossTolerance)); |
123 | else |
124 | // How close are we to the pullback limit? |
125 | signals.add(new CloseSignal("Profit", transformToZeroToOne(profit, |
126 | crest, crest-juicer.pullback)); |
127 | |
128 | ret signals; |
129 | } |
130 | } |
131 | |
132 | record ClosedPosition(Position p, real closingTime) { |
133 | real profit = Double.NaN; |
134 | |
135 | MPM3 mpm() { ret MPM3.this; } |
136 | |
137 | forwardToPosition |
138 | real closingTime() { ret closingTime; } |
139 | real closingPrice() { ret ticker().priceAtTimestamp(closingTime); } |
140 | |
141 | real duration() { ret closingTime()-openingTime(); } |
142 | |
143 | real profit() { |
144 | if (isNaN(profit)) |
145 | profit = p.profitAtTime(closingTime); |
146 | ret profit; |
147 | } |
148 | } |
149 | |
150 | // Wrappers for L<LivePosition> and L<ClosedPosition> |
151 | // (maybe not needed?) |
152 | |
153 | sclass LivePositions { |
154 | L<LivePosition> positions = syncL(); |
155 | } |
156 | |
157 | sclass ClosedPositions { |
158 | L<ClosedPosition> positions = syncL(); |
159 | } |
160 | |
161 | // BOT (Eye + Juicer) |
162 | |
163 | persistable srecord Juicer(real lossTolerance, real pullback) {} |
164 | |
165 | abstract sclass Eye { |
166 | // returns -1 to open short, 1 to open long |
167 | // and anything in between to do nothing |
168 | abstract real adviseDirection(TickerPoint tickerPoint); |
169 | } |
170 | |
171 | // time = ms to look back |
172 | srecord SimpleEye(long lookbackTime, real minMove) extends Eye { |
173 | // 0 to 1 |
174 | settable real relativeVerificationTime/* = 0.33*/; |
175 | |
176 | // 0 to 1 (roughly) |
177 | settable real verificationThreshold/* = 0.5*/; |
178 | |
179 | // in ms |
180 | real verificationTime() { |
181 | ret lookbackTime*relativeVerificationTime; |
182 | } |
183 | |
184 | // in price percent |
185 | real verificationMoveThreshold() { |
186 | ret minMove*relativeVerificationTime*verificationThreshold; |
187 | } |
188 | |
189 | // in price percent |
190 | real verificationMove(TickerPoint tickerPoint) { |
191 | ret calculateMove(tickerPoint, verificationTime()); |
192 | } |
193 | |
194 | // in price percent, scaled up to lookbackTime |
195 | real relativeVerificationMove(TickerPoint tickerPoint) { |
196 | ret doubleRatio(verificationMove(tickerPoint)*lookbackTime, verificationTime()); |
197 | } |
198 | |
199 | real verificationSignal(TickerPoint tickerPoint) { |
200 | ret doubleRatio(verificationMove(tickerPoint), verificationMoveThreshold()); |
201 | } |
202 | |
203 | bool verificationEnabled() { |
204 | ret relativeVerificationTime != 0; |
205 | } |
206 | |
207 | real adviseDirection(TickerPoint tickerPoint) { |
208 | real move = calculateMove(tickerPoint); |
209 | if (isNaN(move)) ret 0; |
210 | real relativeMove = doubleRatio(move, minMove); |
211 | int sign = sign(relativeMove); |
212 | real absRelativeMove = abs(relativeMove); |
213 | |
214 | if (verificationEnabled()) { |
215 | real verification = max(0, sign*verificationSignal(tickerPoint)); |
216 | absRelativeMove = min(absRelativeMove, verification); |
217 | } |
218 | |
219 | // No more clamping - allow trade signals > 100% |
220 | ret sign*absRelativeMove; |
221 | } |
222 | |
223 | real calculateMove(TickerPoint tickerPoint, real lookbackTime default this.lookbackTime) { |
224 | if (!tickerPoint.canLookback(lround(lookbackTime))) ret Double.NaN; |
225 | real currentPrice = tickerPoint.currentPrice(); |
226 | real before = tickerPoint.lookback(lround(lookbackTime)); |
227 | ret currentPrice/before*100-100; |
228 | } |
229 | } |
230 | |
231 | /*persistable srecord TwoEyes extends Eye { |
232 | settable Eye eye1; |
233 | settable Eye eye2; |
234 | |
235 | *(Eye *eye1, Eye *eye2) {} |
236 | |
237 | real adviseDirection(TickerPoint tickerPoint) { |
238 | } |
239 | }*/ |
240 | |
241 | srecord TradingBot(Eye eye, Juicer juicer) { |
242 | *(real lookbackMinutes, real minMove, real maxLoss, real pullback) { |
243 | eye = new SimpleEye(minutesToMS(lookbackMinutes), minMove); |
244 | juicer = new Juicer(maxLoss, pullback); |
245 | } |
246 | } |
247 | |
248 | class BotInAction extends MetaWithChangeListeners { |
249 | settable TradingBot botConfiguration; |
250 | transient TickerSequence ticker; |
251 | |
252 | // Both open and closed positions may be purely simulated or |
253 | // mirrors of positions actually made IRL (see Position.irlInfo). |
254 | |
255 | new ListWithChangeListeners<LivePosition> openPositions; |
256 | new ListWithChangeListeners<ClosedPosition> closedPositions; |
257 | |
258 | LivePosition openPosition(TickerPoint tickerPoint, real direction) { |
259 | var p = new Position(tickerPoint.ticker(), tickerPoint.time(), direction); |
260 | var livePosition = new LivePosition(p).juicer(botConfiguration.juicer); |
261 | livePosition.update(tickerPoint.time()); |
262 | openPositions.add(livePosition); |
263 | change(); |
264 | ret livePosition; |
265 | } |
266 | |
267 | void closePosition(LivePosition position) { |
268 | if (openPositions.remove(position)) { |
269 | // TODO: closedPositions.add... |
270 | change(); |
271 | } |
272 | } |
273 | |
274 | void restoreTicker(TickerSequence ticker) { |
275 | if (ticker == this.ticker) ret; |
276 | this.ticker = ticker; |
277 | ticker.onPricePointAdded(time -> updatePositions(time)); |
278 | for (p : openPositions) p.restoreTicker(ticker); |
279 | for (p : closedPositions) p.restoreTicker(ticker); |
280 | } |
281 | |
282 | void updatePositions(real time) { |
283 | bool anyChange; |
284 | for (p : openPositions) anyChange |= p.update(time); |
285 | if (anyChange) change(); |
286 | } |
287 | } |
288 | |
289 | ClosedPosition runJuicer(Position p, Juicer j) { |
290 | TickerSequence ticker = p.ticker; |
291 | |
292 | long time = lround(p.openingTime); |
293 | double maxClosingTime = min(time+maxPositionDuration, ticker.endTime()); |
294 | real crest = -infinity(); |
295 | |
296 | while (time < maxClosingTime) { |
297 | time = ticker.nextTimestamp(time); |
298 | real profit = p.profitAtTime(time); |
299 | crest = max(crest, profit); |
300 | if (profit < (profit < 0 ? -j.lossTolerance : crest-j.pullback)) |
301 | break; |
302 | } |
303 | |
304 | ret new ClosedPosition(p, time); |
305 | } |
306 | |
307 | record noeq Backtest(TickerSequence ticker, TradingBot bot) extends Convergent { |
308 | new L<ClosedPosition> closedPositions; |
309 | |
310 | long time; |
311 | |
312 | double latestAllowedOpeningTime() { |
313 | ret ticker.endTime()-maxPositionDuration; |
314 | } |
315 | |
316 | void step { |
317 | if (time == 0) time = ticker.startTime(); |
318 | if (time > latestAllowedOpeningTime()) |
319 | ret with done = true; |
320 | |
321 | TickerPoint tickerPoint = new(ticker, time); |
322 | real direction = bot.eye.adviseDirection(tickerPoint); |
323 | if (abs(direction) < 1) |
324 | // No position to open, move to next timestamp |
325 | time = ticker.nextTimestamp(time); |
326 | else { |
327 | // We have a position to open |
328 | var position = new Position(ticker, time, direction) |
329 | .eye(bot.eye); |
330 | |
331 | // Ask juicer when to close position |
332 | var closedPosition = runJuicer(position, bot.juicer); |
333 | |
334 | // Add to record of positions made |
335 | closedPositions.add(closedPosition); |
336 | |
337 | // Move on to next timestamp |
338 | time = ticker.nextTimestamp(closedPosition.closingTime); |
339 | } |
340 | } |
341 | } |
342 | } |
Began life as a copy of #1036148
download show line numbers debug dex old transpilations
Travelled to 3 computer(s): elmgxqgtpvxh, mqqgnosmbjvj, wnsclhtenguj
No comments. add comment
Snippet ID: | #1036164 |
Snippet name: | MPM3 - Trading Bot v3 |
Eternal ID of this version: | #1036164/95 |
Text MD5: | 60307496b119b8815b3b635d9ce80fc8 |
Author: | stefan |
Category: | javax / trading bot |
Type: | JavaX fragment (include) |
Public (visible to everyone): | Yes |
Archived (hidden from active list): | No |
Created/modified: | 2022-10-30 18:05:14 |
Source code size: | 10946 bytes / 342 lines |
Pitched / IR pitched: | No / No |
Views / Downloads: | 281 / 703 |
Version history: | 94 change(s) |
Referenced in: | [show references] |