Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
0d9bc58
feat(tuner): tape-replay model for the Entry/Exit axis, MoonShot entr…
guyverino Sep 20, 2026
b67c7d9
feat(tuner): deals and held-tape reads for the Entry/Exit axis
guyverino Sep 20, 2026
e64b143
feat(tuner): Entry/Exit axis UI (phase 1), the sell-line rules and th…
guyverino Sep 20, 2026
9bbf168
feat(tuner): variants, search and save for the Entry/Exit axis (phase 2)
guyverino Sep 20, 2026
1df2a80
feat(tuner): background tape fetch for the Entry/Exit axis, held tape…
guyverino Sep 20, 2026
1611270
feat(tuner): tape fetch in parallel lanes, clusters and startup autol…
guyverino Sep 20, 2026
7df3dab
feat(storage): trade-replay margin in seconds on a step list (10 s … …
guyverino Sep 20, 2026
23f113f
feat(tuner): deal table of the Entry/Exit axis — USDT profit, held ta…
guyverino Sep 20, 2026
fdea060
fix(trade-replay): stop refusing Gate futures pages the venue served …
guyverino Sep 21, 2026
257969a
feat(storage): 5 s trade margin by default, a long position from 5 mi…
guyverino Sep 21, 2026
69f7170
feat(trade-replay): file the core's ring into the tiles for a tiles r…
guyverino Sep 21, 2026
b1a361e
feat(storage): trade-tape cleanup on the Storage tab and at startup, …
guyverino Sep 21, 2026
1396835
fix(trade-replay): walk Gate futures trades by time and id, not by of…
guyverino Sep 21, 2026
9a5352c
feat(tuner): judge the exit by the line's level at the close, take th…
guyverino Sep 21, 2026
a7876fc
feat(tuner): core column, status line and a "tunable only" switch on …
guyverino Sep 21, 2026
bbb3ffa
feat(tuner): MoonHook's own take, the delta modifiers on sell and sto…
guyverino Sep 22, 2026
aa1df88
feat(tuner): the book-watching stop, UseStopLoss, and the archive's f…
guyverino Sep 22, 2026
d6e6f0c
feat(tuner): chain PriceDown off the placed price, judge the fill on …
guyverino Sep 22, 2026
41d280a
feat(tuner): search only the trades the model reproduces, lean varian…
guyverino Sep 23, 2026
4c17cbd
feat(tuner): open the model at the entry order's creation, re-place a…
guyverino Sep 23, 2026
3adaf38
fix(trade-replay): one tape margin with a 30 s floor, and a trade pas…
guyverino Sep 23, 2026
4938553
feat(tuner): re-evaluate the coin deltas along the trade as the core …
guyverino Sep 23, 2026
67cd7a1
feat(tuner): re-evaluate every delta the history can give, and show h…
guyverino Sep 23, 2026
8cb4263
feat(tuner): read the stop off the ticker, divide a short MoonShot ta…
guyverino Sep 23, 2026
bcc144a
feat(tuner): read the long ranges off closed candles and re-seed BTC'…
guyverino Sep 23, 2026
59671c0
feat(tuner): replay a MoonShot variant's entry by the corridor model …
guyverino Sep 23, 2026
7e3372c
feat(tuner): give the Entry/Exit axis the By-filter panel and setting…
guyverino Sep 23, 2026
3d8a594
test(trade-replay): move main's Gate trade tests onto the time/id fut…
guyverino Sep 23, 2026
e4cefec
feat(tuner): show the selected deal under the Entry/Exit table with t…
guyverino Sep 23, 2026
0cc43bf
fix(chart): end a closed trade's archived exit line at the report's sale
guyverino Sep 23, 2026
c307ed5
fix(tuner): replay each deal under its own strategy, keep the search …
guyverino Sep 23, 2026
282c67d
feat(tuner): keep the corridor, move distance in pairs and say how a …
guyverino Sep 23, 2026
5855800
feat(tuner): lay the Entry/Exit grid out by the Strategies window's s…
guyverino Sep 24, 2026
d363307
feat(tuner): split the exit by the Strategies window's sections, mode…
guyverino Sep 24, 2026
2794b1b
feat(tuner): divide a short's levels, drop SellShot/SellSpread, warn …
guyverino Sep 24, 2026
04d7c96
feat(tuner): stop ladder and Stops knobs, honest search, search survi…
guyverino Sep 24, 2026
6421f4e
feat(tuner): grid shows fields in use, honest base, tail filter, fine…
guyverino Sep 24, 2026
2cfe077
chore(tuner): drop the duplicates the rebase onto #717 left, keep the…
guyverino Sep 24, 2026
1cecd14
feat(tuner): search the Delta Modifiers section
guyverino Sep 25, 2026
711d9ad
feat(tuner): search ranges from the live strategies, typed over in th…
guyverino Sep 25, 2026
7d6a0e7
fix(tuner): every search runs on from V1 and lands over it
guyverino Sep 25, 2026
a1be717
feat(tuner): nest the exit search under every entry point, estimate t…
guyverino Sep 25, 2026
92463d2
feat(tuner): model accuracy under the grid, the model's sell path in …
guyverino Sep 25, 2026
58911f0
feat(gap): introduce TapeGap to handle holes in long positions
guyverino Sep 25, 2026
acc77fc
fix(tuner): the price grid off the tape, the hook's modifier sum and …
guyverino Sep 26, 2026
4f92082
fix(tuner): a searched value moves off the strategy as a value, not a…
guyverino Sep 26, 2026
6a42a8e
fix(tuner): read the pre-spike ask off the last taker buy
guyverino Sep 26, 2026
95ebef0
docs(tuner): code comments caught up with the model
guyverino Sep 26, 2026
9fdc382
feat(tuner): the share gate warns instead of locking a group out
guyverino Sep 26, 2026
a4efe35
chore(tuner): clear the clippy findings the branch added
guyverino Sep 26, 2026
2b61192
docs(product-map): list the Entry/Exit tuner axis
guyverino Sep 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions assets/param_deps.toml
Original file line number Diff line number Diff line change
Expand Up @@ -215,11 +215,12 @@
"PriceDownPercent" = "AutoSell=YES;PriceDownTimer<>0"
"PriceDownRelative" = "AutoSell=YES;PriceDownTimer<>0"
"PriceDownTimer" = "HODLmode=NO;AutoSell=YES"
"PriceDownToAllowedDrop" = "AutoSell=YES;PriceDownTimer<>0;PriceDownRelative=YES"
"SellByCustomEMA" = "AutoSell=YES"
"SellByFilters" = "AutoSell=YES"
"SellDelay" = "HODLmode=NO;AutoSell=YES"
"SellEMACheckEnter" = "AutoSell=YES"
"SellEMADelay" = "AutoSell=YES"
"SellEMACheckEnter" = "AutoSell=YES;IgnoreFilters=NO;IgnoreBase=NO;CustomEMA<>"
"SellEMADelay" = "AutoSell=YES;SellByCustomEMA<>"
"SellFromAssets" = "AutoSell=YES"
"SellLevelAdjust" = "AutoSell=YES;SellLevelDelay<>0;SellLevelTime<>0"
"SellLevelAllowedDrop" = "AutoSell=YES;SellLevelDelay<>0;SellLevelTime<>0"
Expand Down Expand Up @@ -395,7 +396,7 @@
"MShotRepeatWait" = ""
"MShotReplaceDelay" = ""
"MShotSellAtLastPrice" = ""
"MShotSellPriceAdjust" = ""
"MShotSellPriceAdjust" = "MShotSellAtLastPrice=YES"
"MShotSortBy" = ""
"MShotSortDesc" = ""
"MShotUsePrice" = ""
Expand Down
152 changes: 102 additions & 50 deletions crates/moon-chart/src/frozen_overlay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ pub struct OverlayTrade {
pub fill_price: f32,
/// Exit instant and price; `None` when the position was still open where the tape ends.
pub exit: Option<(f64, f32)>,
/// The sell order's path as the model walked it — `(Unix UTC ms, level)` of each placement
/// from the fill, stepped to the exit like the entry path; empty draws the exit line flat at
/// the exit price, as a Moonbot line pair does.
pub exit_path: Vec<(f64, f32)>,
/// Whether the position is short, which picks the short styles and the arrows' direction.
pub is_short: bool,
/// Pen of the entry path, the exit line and the connector (`SEG_PATTERN_*`) — what tells two
Expand Down Expand Up @@ -135,47 +139,19 @@ pub fn build_overlay_geometry(
} else {
(&style.buy, &style.sell)
};
// The entry line the model walked, stepped like a repriced order's path, to the fill.
// The entry line the model walked, to the fill.
let entry_color = rgba(entry_style.color, 1.0);
for (i, &(t_ms, level)) in trade.path.iter().enumerate() {
if !price_ok(level) || t_ms > trade.fill_ms {
continue;
}
let end_ms = trade
.path
.get(i + 1)
.map_or(trade.fill_ms, |(next_ms, _)| *next_ms)
.min(trade.fill_ms)
.max(t_ms);
segs.push(SegInstance {
t0_rel: to_rel(t_ms),
p0: level,
t1_rel: to_rel(end_ms),
p1: level,
push_stepped(
segs,
&trade.path,
Pen {
end_ms: trade.fill_ms,
thickness: entry_style.thickness,
pattern: trade.pattern,
extend: SEG_EXTEND_NONE,
clamp: SEG_CLAMP_NONE,
color: entry_color,
});
let riser = trade
.path
.get(i + 1)
.filter(|(next_ms, next)| *next_ms <= trade.fill_ms && price_ok(*next));
if let Some(&(next_ms, next)) = riser {
segs.push(SegInstance {
t0_rel: to_rel(next_ms),
p0: level,
t1_rel: to_rel(next_ms),
p1: next,
thickness: 1.0,
pattern: trade.pattern,
extend: SEG_EXTEND_NONE,
clamp: SEG_CLAMP_NONE,
color: entry_color,
});
}
}
epoch_ms,
},
);
// Direction follows the ACTION, as trade history's arrows do: a long enters with a buy.
markers.push(arrow(
trade.fill_ms,
Expand All @@ -187,19 +163,42 @@ pub fn build_overlay_geometry(
continue;
};
let exit_color = rgba(exit_style.color, 1.0);
// The exit line as a Moonbot line pair draws it: at the exit price, from the fill (where
// the exit order is placed) to the close.
segs.push(SegInstance {
t0_rel: to_rel(trade.fill_ms),
p0: exit_price,
t1_rel: to_rel(exit_ms.max(trade.fill_ms)),
p1: exit_price,
thickness: exit_style.thickness,
pattern: trade.pattern,
extend: SEG_EXTEND_NONE,
clamp: SEG_CLAMP_NONE,
color: exit_color,
});
let exit_end_ms = exit_ms.max(trade.fill_ms);
// A path none of whose levels stood before the close — a stop inside `SellDelay`, before
// the sell was ever placed, or on the very ms it was — draws as a path without one would.
let path_drawn = trade
.exit_path
.iter()
.any(|&(t_ms, level)| t_ms < exit_end_ms && price_ok(level));
if !path_drawn {
// The exit line as a Moonbot line pair draws it: at the exit price, from the fill
// (where the exit order is placed) to the close.
segs.push(SegInstance {
t0_rel: to_rel(trade.fill_ms),
p0: exit_price,
t1_rel: to_rel(exit_end_ms),
p1: exit_price,
thickness: exit_style.thickness,
pattern: trade.pattern,
extend: SEG_EXTEND_NONE,
clamp: SEG_CLAMP_NONE,
color: exit_color,
});
} else {
// The sell order the model walked, from its placement to the close — a stop's exit
// lands off it, at the stop's own price.
push_stepped(
segs,
&trade.exit_path,
Pen {
end_ms: exit_end_ms,
thickness: exit_style.thickness,
pattern: trade.pattern,
color: exit_color,
epoch_ms,
},
);
}
segs.push(SegInstance {
t0_rel: to_rel(trade.fill_ms),
p0: trade.fill_price,
Expand All @@ -215,5 +214,58 @@ pub fn build_overlay_geometry(
}
}

/// How [`push_stepped`] draws one path.
struct Pen {
/// Nothing is drawn past this instant, Unix UTC ms.
end_ms: f64,
thickness: f32,
pattern: f32,
color: [f32; 4],
/// The pane's epoch; every instance is relative to it.
epoch_ms: f64,
}

/// A path the model walked, stepped like a repriced order's: a level per placement to the next
/// one, a riser at each move, nothing past the pen's end.
fn push_stepped(segs: &mut Vec<SegInstance>, path: &[(f64, f32)], pen: Pen) {
let to_rel = |t_ms: f64| (t_ms - pen.epoch_ms) as f32;
let price_ok = |p: f32| p.is_finite() && p > 0.0;
for (i, &(t_ms, level)) in path.iter().enumerate() {
if !price_ok(level) || t_ms > pen.end_ms {
continue;
}
let next = path.get(i + 1);
let to_ms = next
.map_or(pen.end_ms, |(next_ms, _)| *next_ms)
.min(pen.end_ms)
.max(t_ms);
segs.push(SegInstance {
t0_rel: to_rel(t_ms),
p0: level,
t1_rel: to_rel(to_ms),
p1: level,
thickness: pen.thickness,
pattern: pen.pattern,
extend: SEG_EXTEND_NONE,
clamp: SEG_CLAMP_NONE,
color: pen.color,
});
let riser = next.filter(|(next_ms, next)| *next_ms <= pen.end_ms && price_ok(*next));
if let Some(&(next_ms, next)) = riser {
segs.push(SegInstance {
t0_rel: to_rel(next_ms),
p0: level,
t1_rel: to_rel(next_ms),
p1: next,
thickness: 1.0,
pattern: pen.pattern,
extend: SEG_EXTEND_NONE,
clamp: SEG_CLAMP_NONE,
color: pen.color,
});
}
}
}

#[cfg(test)]
mod tests;
70 changes: 70 additions & 0 deletions crates/moon-chart/src/frozen_overlay/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ fn a_modelled_trade_carries_its_pen_and_an_open_one_only_its_entry() {
fill_ms: 3_000.0,
fill_price: 1.0,
exit: Some((6_000.0, 1.05)),
exit_path: Vec::new(),
is_short: false,
pattern: SEG_PATTERN_DASH,
},
Expand All @@ -81,6 +82,7 @@ fn a_modelled_trade_carries_its_pen_and_an_open_one_only_its_entry() {
fill_ms: 3_500.0,
fill_price: 1.01,
exit: None,
exit_path: Vec::new(),
is_short: true,
pattern: SEG_PATTERN_DOT,
},
Expand Down Expand Up @@ -114,6 +116,7 @@ fn a_modelled_entry_path_steps_to_the_fill() {
fill_ms: 3_000.0,
fill_price: 0.99,
exit: None,
exit_path: Vec::new(),
is_short: false,
pattern: SEG_PATTERN_DASH,
}],
Expand All @@ -135,3 +138,70 @@ fn a_modelled_entry_path_steps_to_the_fill() {
"one riser: the second move falls after the fill"
);
}

/// The model's sell path is stepped from its placement to the close, like the entry path, and
/// stands in for the flat exit line; the exit arrow keeps the exit's own price.
#[test]
fn a_modelled_sell_path_steps_to_the_close() {
let (_, segs, markers) = build(&FrozenOverlay {
bands: Vec::new(),
trades: vec![OverlayTrade {
path: Vec::new(),
fill_ms: 2_000.0,
fill_price: 1.0,
exit: Some((5_000.0, 1.02)),
exit_path: vec![
(2_000.0, 1.05),
(3_000.0, 1.03),
(4_000.0, 1.02),
(6_000.0, 1.01),
],
is_short: false,
pattern: SEG_PATTERN_DASH,
}],
});
assert_eq!(markers.len(), 2, "the entry and the exit arrow");
let levels: Vec<(f32, f32, f32)> = segs
.iter()
.filter(|s| s.p0 == s.p1)
.map(|s| (s.t0_rel, s.t1_rel, s.p0))
.collect();
assert_eq!(
levels,
vec![
(1_000.0, 2_000.0, 1.05),
(2_000.0, 3_000.0, 1.03),
(3_000.0, 4_000.0, 1.02),
],
"no flat line at the exit price, and nothing past the close"
);
assert_eq!(
segs.iter().filter(|s| s.p0 != s.p1).count(),
3,
"two risers and the fill-to-exit connector"
);
}

/// A sell path none of whose levels stood by the close — a stop inside `SellDelay`, the sell not
/// yet placed — draws the flat exit line rather than nothing.
#[test]
fn a_sell_path_placed_after_the_close_falls_back_to_the_flat_line() {
let (_, segs, _) = build(&FrozenOverlay {
bands: Vec::new(),
trades: vec![OverlayTrade {
path: Vec::new(),
fill_ms: 2_000.0,
fill_price: 1.0,
exit: Some((3_000.0, 0.98)),
exit_path: vec![(4_000.0, 1.05)],
is_short: false,
pattern: SEG_PATTERN_DASH,
}],
});
let levels: Vec<(f32, f32, f32)> = segs
.iter()
.filter(|s| s.p0 == s.p1)
.map(|s| (s.t0_rel, s.t1_rel, s.p0))
.collect();
assert_eq!(levels, vec![(1_000.0, 2_000.0, 0.98)]);
}
Loading
Loading