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 | real openingPrice() { ret ticker.priceAtTimestamp(openingTime); } |
42 | |
43 | real profitAtTime(real time) { |
44 | ret (ticker.priceAtTimestamp(time)/openingPrice()*100-100)*direction - marketAdversity; |
45 | } |
46 | |
47 | real openingTime() { ret openingTime; } |
48 | |
49 | S directionString() { ret direction > 0 ? "long" : direction < 0 ? "short" : ""; } |
50 | |
51 | void restoreTicker(TickerSequence ticker) { this.ticker = ticker; } |
52 | } |
53 | |
54 | macro forwardToPosition { |
55 | TickerSequence ticker() { ret p.ticker; } |
56 | void restoreTicker(TickerSequence ticker) { p.restoreTicker(ticker); } |
57 | real direction() { ret p.direction; } |
58 | S directionString() { ret p.directionString(); } |
59 | real openingTime() { ret p.openingTime; } |
60 | real openingPrice() { ret p.openingPrice(); } |
61 | } |
62 | |
63 | persistable record LivePosition(Position p) { |
64 | settable Juicer juicer; |
65 | |
66 | // highest profit this position has reached |
67 | settable real crest = -infinity(); |
68 | |
69 | settable real time; // time we last updated this position |
70 | settable real profit; // expected profit at that time |
71 | |
72 | forwardToPosition |
73 | |
74 | // return true if any change |
75 | bool update(real time) { |
76 | if (time <= this.time) false; |
77 | this.time = time; |
78 | profit = p.profitAtTime(time); |
79 | crest = max(crest, profit); |
80 | true; |
81 | } |
82 | |
83 | transient simplyCached real maxClosingTime() { |
84 | ret p.ticker.openEnded() ? infinity() |
85 | : min(p.openingTime+maxPositionDuration, p.ticker.endTime()); |
86 | } |
87 | |
88 | bool shouldClose() { |
89 | ret time >= maxClosingTime() |
90 | || profit < (profit < 0 ? -juicer.lossTolerance : crest-juicer.pullback); |
91 | } |
92 | |
93 | // >= 1 to close |
94 | // otherwise keep open |
95 | real closeSignal() { |
96 | CloseSignal strongestSignal = closeSignalWithDescription(); |
97 | ret strongestSignal == null ? 0 : strongestSignal.trigger; |
98 | } |
99 | |
100 | CloseSignal closeSignalWithDescription() { |
101 | ret highestBy(closeSignals(), signal -> signal.trigger); |
102 | } |
103 | |
104 | // trigger = 0 to 1 |
105 | record noeq CloseSignal(S reason, real trigger) {} |
106 | |
107 | L<CloseSignal> closeSignals() { |
108 | new L<CloseSignal> signals; |
109 | |
110 | // How close are we to the max closing time? |
111 | if (maxClosingTime() < infinity()) |
112 | signals.add(new CloseSignal("Age", transformToZeroToOne(time, p.openingTime(), maxClosingTime()))); |
113 | |
114 | // How close are we to our loss limit? |
115 | if (profit < 0) |
116 | signals.add(new CloseSignal("Loss", doubleRatio(-profit, juicer.lossTolerance)); |
117 | else |
118 | // How close are we to the pullback limit? |
119 | signals.add(new CloseSignal("Profit", transformToZeroToOne(profit, |
120 | crest, crest-juicer.pullback)); |
121 | |
122 | ret signals; |
123 | } |
124 | } |
125 | |
126 | record ClosedPosition(Position p, real closingTime) { |
127 | real profit = Double.NaN; |
128 | |
129 | MPM3 mpm() { ret MPM3.this; } |
130 | |
131 | forwardToPosition |
132 | real closingTime() { ret closingTime; } |
133 | real closingPrice() { ret ticker().priceAtTimestamp(closingTime); } |
134 | |
135 | real duration() { ret closingTime()-openingTime(); } |
136 | |
137 | real profit() { |
138 | if (isNaN(profit)) |
139 | profit = p.profitAtTime(closingTime); |
140 | ret profit; |
141 | } |
142 | } |
143 | |
144 | // Wrappers for L<LivePosition> and L<ClosedPosition> |
145 | // (maybe not needed?) |
146 | |
147 | sclass LivePositions { |
148 | L<LivePosition> positions = syncL(); |
149 | } |
150 | |
151 | sclass ClosedPositions { |
152 | L<ClosedPosition> positions = syncL(); |
153 | } |
154 | |
155 | // BOT (Eye + Juicer) |
156 | |
157 | persistable srecord Juicer(real lossTolerance, real pullback) {} |
158 | |
159 | abstract sclass Eye { |
160 | // returns -1 to open short, 1 to open long |
161 | // and anything in between to do nothing |
162 | abstract real adviseDirection(TickerPoint tickerPoint); |
163 | } |
164 | |
165 | // time = ms to look back |
166 | srecord SimpleEye(long lookbackTime, real minMove) extends Eye { |
167 | real adviseDirection(TickerPoint tickerPoint) { |
168 | real move = calculateMove(tickerPoint); |
169 | if (isNaN(move)) ret 0; |
170 | real relativeMove = doubleRatio(move, minMove); |
171 | ret clamp(relativeMove, -1, 1); |
172 | } |
173 | |
174 | real calculateMove(TickerPoint tickerPoint) { |
175 | if (!tickerPoint.canLookback(lookbackTime)) ret Double.NaN; |
176 | real currentPrice = tickerPoint.currentPrice(); |
177 | real before = tickerPoint.lookback(lookbackTime); |
178 | ret currentPrice/before*100-100; |
179 | } |
180 | } |
181 | |
182 | srecord TradingBot(Eye eye, Juicer juicer) { |
183 | *(real lookbackMinutes, real minMove, real maxLoss, real pullback) { |
184 | eye = new SimpleEye(minutesToMS(lookbackMinutes), minMove); |
185 | juicer = new Juicer(maxLoss, pullback); |
186 | } |
187 | } |
188 | |
189 | class BotInAction extends MetaWithChangeListeners { |
190 | settable TradingBot botConfiguration; |
191 | transient TickerSequence ticker; |
192 | |
193 | // Both open and closed positions may be purely simulated or |
194 | // mirrors of positions actually made IRL (see Position.irlInfo). |
195 | |
196 | new ListWithChangeListeners<LivePosition> openPositions; |
197 | new ListWithChangeListeners<ClosedPosition> closedPositions; |
198 | |
199 | LivePosition openPosition(TickerPoint tickerPoint, real direction) { |
200 | var p = new Position(tickerPoint.ticker(), tickerPoint.time(), direction); |
201 | var livePosition = new LivePosition(p).juicer(botConfiguration.juicer); |
202 | livePosition.update(tickerPoint.time()); |
203 | openPositions.add(livePosition); |
204 | change(); |
205 | ret livePosition; |
206 | } |
207 | |
208 | void closePosition(LivePosition position) { |
209 | if (openPositions.remove(position)) { |
210 | // TODO: closedPositions.add... |
211 | change(); |
212 | } |
213 | } |
214 | |
215 | void restoreTicker(TickerSequence ticker) { |
216 | if (ticker == this.ticker) ret; |
217 | this.ticker = ticker; |
218 | ticker.onPricePointAdded(time -> updatePositions(time)); |
219 | for (p : openPositions) p.restoreTicker(ticker); |
220 | for (p : closedPositions) p.restoreTicker(ticker); |
221 | } |
222 | |
223 | void updatePositions(real time) { |
224 | bool anyChange; |
225 | for (p : openPositions) anyChange |= p.update(time); |
226 | if (anyChange) change(); |
227 | } |
228 | } |
229 | |
230 | ClosedPosition runJuicer(Position p, Juicer j) { |
231 | TickerSequence ticker = p.ticker; |
232 | |
233 | long time = lround(p.openingTime); |
234 | double maxClosingTime = min(time+maxPositionDuration, ticker.endTime()); |
235 | real crest = -infinity(); |
236 | |
237 | while (time < maxClosingTime) { |
238 | time = ticker.nextTimestamp(time); |
239 | real profit = p.profitAtTime(time); |
240 | crest = max(crest, profit); |
241 | if (profit < (profit < 0 ? -j.lossTolerance : crest-j.pullback)) |
242 | break; |
243 | } |
244 | |
245 | ret new ClosedPosition(p, time); |
246 | } |
247 | |
248 | record noeq Backtest(TickerSequence ticker, TradingBot bot) extends Convergent { |
249 | new L<ClosedPosition> closedPositions; |
250 | |
251 | long time; |
252 | |
253 | double latestAllowedOpeningTime() { |
254 | ret ticker.endTime()-maxPositionDuration; |
255 | } |
256 | |
257 | void step { |
258 | if (time == 0) time = ticker.startTime(); |
259 | if (time > latestAllowedOpeningTime()) |
260 | ret with done = true; |
261 | |
262 | TickerPoint tickerPoint = new(ticker, time); |
263 | real direction = bot.eye.adviseDirection(tickerPoint); |
264 | if (abs(direction) < 1) |
265 | // No position to open, move to next timestamp |
266 | time = ticker.nextTimestamp(time); |
267 | else { |
268 | // We have a position to open |
269 | var position = new Position(ticker, time, direction); |
270 | |
271 | // Ask juicer when to close position |
272 | var closedPosition = runJuicer(position, bot.juicer); |
273 | |
274 | // Add to record of positions made |
275 | closedPositions.add(closedPosition); |
276 | |
277 | // Move on to next timestamp |
278 | time = ticker.nextTimestamp(closedPosition.closingTime); |
279 | } |
280 | } |
281 | } |
282 | } |
Began life as a copy of #1036164
download show line numbers debug dex old transpilations
Travelled to 1 computer(s): mqqgnosmbjvj
No comments. add comment
Snippet ID: | #1036185 |
Snippet name: | MPM3 - backup before extensions |
Eternal ID of this version: | #1036185/1 |
Text MD5: | 5aaf46284f2bf0769f160f52001b4b95 |
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-10 15:30:12 |
Source code size: | 9101 bytes / 282 lines |
Pitched / IR pitched: | No / No |
Views / Downloads: | 128 / 140 |
Referenced in: | [show references] |