diff --git a/app/api/routes.py b/app/api/routes.py index 2b2461a..62c92de 100644 --- a/app/api/routes.py +++ b/app/api/routes.py @@ -341,6 +341,8 @@ def patch_line(request: Request, line_id: str, payload: LinePatch): # None deliberately clears a per-level override and restores ATR behavior. if "alert_early_points" in payload.model_fields_set: changes["alert_early_points"] = payload.alert_early_points + if "cutoff_t" in payload.model_fields_set: + changes["cutoff_t"] = payload.cutoff_t if changes.get("anchor_t") == changes.get("last_t") and "anchor_t" in changes: raise HTTPException(400, "Line endpoints must have different times") try: diff --git a/static/app.js b/static/app.js index fb03437..077341c 100644 --- a/static/app.js +++ b/static/app.js @@ -131,6 +131,7 @@ createApp({ const comments = ref([]); const drawingFilter = ref(''); const drawingKind = ref('all'); + const drawingTf = ref('all'); const diagnosticMode = ref(ConfluenceChart.diagnosticMode()); const captureBusy = ref(false); const snap = ref(true); @@ -284,10 +285,11 @@ createApp({ const needle = drawingFilter.value.trim().toLowerCase(); return drawings.value.filter(item => { if (drawingKind.value !== 'all' && item.kind !== drawingKind.value) return false; + const timeframe = item.line?.tf || item.comment?.tf || ''; + if (drawingTf.value !== 'all' && timeframe !== drawingTf.value) return false; if (!needle) return true; // Matched against what is on screen — the label or comment text, the // kind, and the drawing number, so "comment", "cpi" and "7" all work. - const timeframe = item.line?.tf || item.comment?.tf || ''; return `${item.label} ${item.kind} ${timeframe} #${item.number}`.toLowerCase().includes(needle); }); }); @@ -1223,6 +1225,6 @@ createApp({ window.removeEventListener('keydown', handleKeydown); }); - return { status, price, sessionOpen, quoteChange, animateCurrentPrice, autoScrollLivePrice, barAge, dataUpdatedAt, buildStamp, timeframe, timeframes, drawingColors, drawingColorRows, drawingColorName, colorRowLabels, symbolChoices, selectedSymbol, symbolColor, symbolScale, symbolScales, symbolPanelOpen, prefs, clusters, clustersByPrice, events, diagnosticMode, captureBusy, captureDiagnostic, armedTool, drawName, drawColor, drawWidth, drawSide, snap, selectedDrawing, selectedDrawings, drawingList, startDrawingListResize, manualLines, hasDrawingSelection, allShownSelected, selectedAreHidden, alertPrice, alertNote, alertEarlyPoints, levelColor, levelWidth, addPriceAlert, armTool, selectTimeframe, allEnabled, toggleGroup, deleteSelected, deleteLine, toggleDrawingSelection, toggleSelectAll, toggleSelectedVisibility, renameLine, updateLineStyle, updateLevelNumber, setArmed, commentText, commentFloat, comments, drawings, filteredDrawings, drawingFilter, drawingKind, deleteDrawing, toggleComment, togglePinned, chooseSymbol, toggleSymbolPanel, startSymbolDrag, dropSymbol, optionExpirations, optionExpiryId, optionSide, optionMode, optionMin, optionMax, optionContracts, optionUnderlying, optionBusy, optionError, optionSearched, optionCopied, onOptionsToggle, searchOptions, copyOption }; + return { status, price, sessionOpen, quoteChange, animateCurrentPrice, autoScrollLivePrice, barAge, dataUpdatedAt, buildStamp, timeframe, timeframes, drawingColors, drawingColorRows, drawingColorName, colorRowLabels, symbolChoices, selectedSymbol, symbolColor, symbolScale, symbolScales, symbolPanelOpen, prefs, clusters, clustersByPrice, events, diagnosticMode, captureBusy, captureDiagnostic, armedTool, drawName, drawColor, drawWidth, drawSide, snap, selectedDrawing, selectedDrawings, drawingList, startDrawingListResize, manualLines, hasDrawingSelection, allShownSelected, selectedAreHidden, alertPrice, alertNote, alertEarlyPoints, levelColor, levelWidth, addPriceAlert, armTool, selectTimeframe, allEnabled, toggleGroup, deleteSelected, deleteLine, toggleDrawingSelection, toggleSelectAll, toggleSelectedVisibility, renameLine, updateLineStyle, updateLevelNumber, setArmed, commentText, commentFloat, comments, drawings, filteredDrawings, drawingFilter, drawingKind, drawingTf, deleteDrawing, toggleComment, togglePinned, chooseSymbol, toggleSymbolPanel, startSymbolDrag, dropSymbol, optionExpirations, optionExpiryId, optionSide, optionMode, optionMin, optionMax, optionContracts, optionUnderlying, optionBusy, optionError, optionSearched, optionCopied, onOptionsToggle, searchOptions, copyOption }; }, }).mount('#app'); diff --git a/static/chart.js b/static/chart.js index eb9a6cb..0d0f50c 100644 --- a/static/chart.js +++ b/static/chart.js @@ -40,6 +40,7 @@ class ConfluenceChart { this.contextCutoff = null; this.contextLineId = null; this.contextEndButton = null; + this.contextExtendButton = null; this.contextListener = null; this.onLineEnd = null; this.onLineDuplicate = null; @@ -319,10 +320,18 @@ class ConfluenceChart { this.endSelectedLineHere(); }); this.contextEndButton = endHere; + const extend = document.createElement('button'); + extend.type = 'button'; + extend.textContent = 'Extend trendline'; + extend.addEventListener('click', event => { + event.stopPropagation(); + this.extendSelectedLine(); + }); + this.contextExtendButton = extend; const duplicate = document.createElement('button'); duplicate.type = 'button'; duplicate.dataset.action = 'duplicate'; - duplicate.textContent = 'Duplicate 10 bars right'; + duplicate.textContent = 'Duplicate'; duplicate.addEventListener('click', event => { event.stopPropagation(); const level = this.levels.find(value => value.id === this.contextLineId); @@ -340,7 +349,7 @@ class ConfluenceChart { if (id && this.onLineDelete) this.onLineDelete(id); }); this.contextMenu.addEventListener('click', event => event.stopPropagation()); - this.contextMenu.append(endHere, duplicate, deleteLine); + this.contextMenu.append(endHere, extend, duplicate, deleteLine); this.overlayLayer.appendChild(this.contextMenu); this.priceTag = document.createElement('div'); this.priceTag.className = 'chart-price-tag'; @@ -2200,6 +2209,7 @@ class ConfluenceChart { this.contextLineId = level.id; this.contextCutoff = cutoff > level.last_t ? cutoff : null; this.contextEndButton.hidden = this.contextCutoff == null; + this.contextExtendButton.hidden = level.cutoff_t == null; this.contextMenu.hidden = false; const plot = this.plotCanvas().getBoundingClientRect(); this.contextMenu.style.left = `${Math.max(4, Math.min(point.x, plot.width - 170))}px`; @@ -2229,6 +2239,17 @@ class ConfluenceChart { if (this.onLineEnd) this.onLineEnd({ ...level }); } + extendSelectedLine() { + const level = this.levels.find(value => value.id === this.selectedLineId); + if (!level || level.cutoff_t == null) return; + level.cutoff_t = null; + const entry = this.levelSeries.get(level.id); + if (entry) entry.series.setData(this.seriesLineData(level)); + this.renderLineBridges(); + this.hideContextMenu(); + if (this.onLineEnd) this.onLineEnd({ ...level }); + } + // Tinted by the bar's own direction, muted so the candles stay the subject. static toVolume(bar) { return { diff --git a/static/index.html b/static/index.html index 465d1f3..66a2897 100644 --- a/static/index.html +++ b/static/index.html @@ -231,7 +231,11 @@ - + +