Duplicate a line even when 10 source bars would pass the horizon.
Prod never saw POST /api/lines: the browser aborted the shift. Clamp to the last owned source slot so Duplicate still creates a copy.
This commit is contained in:
parent
6f03560e74
commit
c1d508cb4f
3 changed files with 69 additions and 8 deletions
|
|
@ -1095,6 +1095,14 @@ createApp({
|
|||
return chartApi?.shiftLineTime(line, time, count) ?? null;
|
||||
}
|
||||
|
||||
function duplicateShiftCount(line, count = 10) {
|
||||
const times = [line.anchor_t, line.last_t];
|
||||
if (line.cutoff_t != null) times.push(line.cutoff_t);
|
||||
const allowed = times.map(time => chartApi?.maxLineShift(line, time, count));
|
||||
if (allowed.some(value => value == null)) return null;
|
||||
return Math.min(...allowed);
|
||||
}
|
||||
|
||||
function duplicateName(line) {
|
||||
const match = (line.label || `${line.tf} ${line.side}`).match(/^(.*?)(?: (\d+))?$/);
|
||||
const base = match[1];
|
||||
|
|
@ -1105,15 +1113,35 @@ createApp({
|
|||
}
|
||||
|
||||
async function duplicateLine(line) {
|
||||
const fail = reason => {
|
||||
addEvent(`DUPLICATE #${line.number} FAILED · ${reason}`, 'error');
|
||||
fetch('/api/debug/snap', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
kind: 'duplicate',
|
||||
tf: line.tf,
|
||||
note: `#${line.number} ${reason} last_t ${line.last_t} cutoff ${line.cutoff_t}`,
|
||||
}),
|
||||
}).catch(() => {});
|
||||
};
|
||||
if (line.geometry_resolved === false) {
|
||||
addEvent(`DUPLICATE #${line.number} FAILED · no source geometry`, 'error');
|
||||
fail('no source geometry');
|
||||
return;
|
||||
}
|
||||
const anchorT = shiftTimeByBars(line, line.anchor_t);
|
||||
const endT = shiftTimeByBars(line, line.last_t);
|
||||
const cutoffT = line.cutoff_t == null ? null : shiftTimeByBars(line, line.cutoff_t);
|
||||
const count = duplicateShiftCount(line);
|
||||
if (count == null) {
|
||||
fail('cannot shift');
|
||||
return;
|
||||
}
|
||||
const shift = (time, bars) => (count > 0
|
||||
? shiftTimeByBars(line, time, bars)
|
||||
: chartApi.shiftDisplayedTime(time, 10));
|
||||
const anchorT = shift(line.anchor_t, count);
|
||||
const endT = shift(line.last_t, count);
|
||||
const cutoffT = line.cutoff_t == null ? null : shift(line.cutoff_t, count);
|
||||
if (anchorT == null || endT == null || (line.cutoff_t != null && cutoffT == null)) {
|
||||
addEvent(`DUPLICATE #${line.number} FAILED · cannot shift 10 bars`, 'error');
|
||||
fail('cannot shift');
|
||||
return;
|
||||
}
|
||||
const endP = line.anchor_p + line.slope * (line.last_t - line.anchor_t);
|
||||
|
|
|
|||
|
|
@ -2585,11 +2585,40 @@ class ConfluenceChart {
|
|||
return level.anchor_p + (endPrice - level.anchor_p) * (target - start) / (end - start);
|
||||
}
|
||||
|
||||
sourceIndexAt(level, time) {
|
||||
const source = this.sourceSeries(level);
|
||||
if (!source) return null;
|
||||
const exact = ConfluenceChart.timeframeIndexAt(source, time);
|
||||
if (exact != null) return exact;
|
||||
const times = ConfluenceChart.sourceTimes(source);
|
||||
if (!times.length) return null;
|
||||
let lo = 0;
|
||||
let hi = times.length;
|
||||
while (lo < hi) {
|
||||
const mid = (lo + hi) >> 1;
|
||||
if (times[mid] <= time) lo = mid + 1;
|
||||
else hi = mid;
|
||||
}
|
||||
return lo === 0 ? null : lo - 1;
|
||||
}
|
||||
|
||||
shiftLineTime(level, time, bars) {
|
||||
const source = this.sourceSeries(level);
|
||||
if (!source) return this.shiftDisplayedTime(time, bars);
|
||||
const index = ConfluenceChart.timeframeIndexAt(source, time);
|
||||
return index == null ? null : ConfluenceChart.timeframeTimeAt(source, index + bars);
|
||||
const index = this.sourceIndexAt(level, time);
|
||||
if (index == null) return null;
|
||||
const times = ConfluenceChart.sourceTimes(source);
|
||||
const target = Math.min(index + bars, times.length - 1);
|
||||
if (target < 0) return times[0] ?? null;
|
||||
return ConfluenceChart.timeframeTimeAt(source, target);
|
||||
}
|
||||
|
||||
maxLineShift(level, time, bars) {
|
||||
const index = this.sourceIndexAt(level, time);
|
||||
if (index == null) return null;
|
||||
const source = this.sourceSeries(level);
|
||||
const last = ConfluenceChart.sourceTimes(source).length - 1;
|
||||
return Math.max(0, Math.min(bars, last - index));
|
||||
}
|
||||
|
||||
shiftDisplayedTime(time, slots) {
|
||||
|
|
|
|||
|
|
@ -234,6 +234,8 @@ test('daily future slots and live duration follow the server session calendar',
|
|||
beyondHorizon: ConfluenceChart.timeframeTimeAt(source, source.allTimes.length + 1),
|
||||
shifted: c.shiftDisplayedTime(thursday, 1),
|
||||
shiftBeyondHorizon: c.shiftLineTime(horizonLine, monday, 1),
|
||||
horizonShift: c.maxLineShift(horizonLine, monday, 10),
|
||||
thursdayShift: c.maxLineShift(horizonLine, thursday, 10),
|
||||
};
|
||||
});
|
||||
|
||||
|
|
@ -241,7 +243,9 @@ test('daily future slots and live duration follow the server session calendar',
|
|||
assert.equal(result.duration, 23 * 3600);
|
||||
assert.equal(result.beyondHorizon, null);
|
||||
assert.equal(result.shifted, 1786917600);
|
||||
assert.equal(result.shiftBeyondHorizon, null);
|
||||
assert.equal(result.shiftBeyondHorizon, 1787004000);
|
||||
assert.equal(result.horizonShift, 0);
|
||||
assert.ok(result.thursdayShift > 0);
|
||||
assertNoPageErrors(page, assert);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue