tweaks
This commit is contained in:
parent
3adb98c1fe
commit
11508e5325
6 changed files with 59 additions and 6 deletions
|
|
@ -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.
|
# None deliberately clears a per-level override and restores ATR behavior.
|
||||||
if "alert_early_points" in payload.model_fields_set:
|
if "alert_early_points" in payload.model_fields_set:
|
||||||
changes["alert_early_points"] = payload.alert_early_points
|
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:
|
if changes.get("anchor_t") == changes.get("last_t") and "anchor_t" in changes:
|
||||||
raise HTTPException(400, "Line endpoints must have different times")
|
raise HTTPException(400, "Line endpoints must have different times")
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -131,6 +131,7 @@ createApp({
|
||||||
const comments = ref([]);
|
const comments = ref([]);
|
||||||
const drawingFilter = ref('');
|
const drawingFilter = ref('');
|
||||||
const drawingKind = ref('all');
|
const drawingKind = ref('all');
|
||||||
|
const drawingTf = ref('all');
|
||||||
const diagnosticMode = ref(ConfluenceChart.diagnosticMode());
|
const diagnosticMode = ref(ConfluenceChart.diagnosticMode());
|
||||||
const captureBusy = ref(false);
|
const captureBusy = ref(false);
|
||||||
const snap = ref(true);
|
const snap = ref(true);
|
||||||
|
|
@ -284,10 +285,11 @@ createApp({
|
||||||
const needle = drawingFilter.value.trim().toLowerCase();
|
const needle = drawingFilter.value.trim().toLowerCase();
|
||||||
return drawings.value.filter(item => {
|
return drawings.value.filter(item => {
|
||||||
if (drawingKind.value !== 'all' && item.kind !== drawingKind.value) return false;
|
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;
|
if (!needle) return true;
|
||||||
// Matched against what is on screen — the label or comment text, the
|
// Matched against what is on screen — the label or comment text, the
|
||||||
// kind, and the drawing number, so "comment", "cpi" and "7" all work.
|
// 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);
|
return `${item.label} ${item.kind} ${timeframe} #${item.number}`.toLowerCase().includes(needle);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -1223,6 +1225,6 @@ createApp({
|
||||||
window.removeEventListener('keydown', handleKeydown);
|
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');
|
}).mount('#app');
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ class ConfluenceChart {
|
||||||
this.contextCutoff = null;
|
this.contextCutoff = null;
|
||||||
this.contextLineId = null;
|
this.contextLineId = null;
|
||||||
this.contextEndButton = null;
|
this.contextEndButton = null;
|
||||||
|
this.contextExtendButton = null;
|
||||||
this.contextListener = null;
|
this.contextListener = null;
|
||||||
this.onLineEnd = null;
|
this.onLineEnd = null;
|
||||||
this.onLineDuplicate = null;
|
this.onLineDuplicate = null;
|
||||||
|
|
@ -319,10 +320,18 @@ class ConfluenceChart {
|
||||||
this.endSelectedLineHere();
|
this.endSelectedLineHere();
|
||||||
});
|
});
|
||||||
this.contextEndButton = endHere;
|
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');
|
const duplicate = document.createElement('button');
|
||||||
duplicate.type = 'button';
|
duplicate.type = 'button';
|
||||||
duplicate.dataset.action = 'duplicate';
|
duplicate.dataset.action = 'duplicate';
|
||||||
duplicate.textContent = 'Duplicate 10 bars right';
|
duplicate.textContent = 'Duplicate';
|
||||||
duplicate.addEventListener('click', event => {
|
duplicate.addEventListener('click', event => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
const level = this.levels.find(value => value.id === this.contextLineId);
|
const level = this.levels.find(value => value.id === this.contextLineId);
|
||||||
|
|
@ -340,7 +349,7 @@ class ConfluenceChart {
|
||||||
if (id && this.onLineDelete) this.onLineDelete(id);
|
if (id && this.onLineDelete) this.onLineDelete(id);
|
||||||
});
|
});
|
||||||
this.contextMenu.addEventListener('click', event => event.stopPropagation());
|
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.overlayLayer.appendChild(this.contextMenu);
|
||||||
this.priceTag = document.createElement('div');
|
this.priceTag = document.createElement('div');
|
||||||
this.priceTag.className = 'chart-price-tag';
|
this.priceTag.className = 'chart-price-tag';
|
||||||
|
|
@ -2200,6 +2209,7 @@ class ConfluenceChart {
|
||||||
this.contextLineId = level.id;
|
this.contextLineId = level.id;
|
||||||
this.contextCutoff = cutoff > level.last_t ? cutoff : null;
|
this.contextCutoff = cutoff > level.last_t ? cutoff : null;
|
||||||
this.contextEndButton.hidden = this.contextCutoff == null;
|
this.contextEndButton.hidden = this.contextCutoff == null;
|
||||||
|
this.contextExtendButton.hidden = level.cutoff_t == null;
|
||||||
this.contextMenu.hidden = false;
|
this.contextMenu.hidden = false;
|
||||||
const plot = this.plotCanvas().getBoundingClientRect();
|
const plot = this.plotCanvas().getBoundingClientRect();
|
||||||
this.contextMenu.style.left = `${Math.max(4, Math.min(point.x, plot.width - 170))}px`;
|
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 });
|
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.
|
// Tinted by the bar's own direction, muted so the candles stay the subject.
|
||||||
static toVolume(bar) {
|
static toVolume(bar) {
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -231,7 +231,11 @@
|
||||||
<option value="comment">Comments</option>
|
<option value="comment">Comments</option>
|
||||||
<option value="symbol">Symbols</option>
|
<option value="symbol">Symbols</option>
|
||||||
</select>
|
</select>
|
||||||
<input v-model="drawingFilter" placeholder="Filter by text…" aria-label="Filter drawings by text">
|
<select v-model="drawingTf" aria-label="Filter drawings by timeframe">
|
||||||
|
<option value="all">All TFs</option>
|
||||||
|
<option v-for="tf in timeframes" :key="tf" :value="tf">{{ tf }}</option>
|
||||||
|
</select>
|
||||||
|
<input v-model="drawingFilter" placeholder="Filter text" aria-label="Filter drawings by text">
|
||||||
</div>
|
</div>
|
||||||
<div class="trendline-actions" v-if="drawings.length">
|
<div class="trendline-actions" v-if="drawings.length">
|
||||||
<button @click="toggleSelectAll" :disabled="!filteredDrawings.length">{{ allShownSelected ? 'Clear shown' : 'Select shown' }}</button>
|
<button @click="toggleSelectAll" :disabled="!filteredDrawings.length">{{ allShownSelected ? 'Clear shown' : 'Select shown' }}</button>
|
||||||
|
|
|
||||||
|
|
@ -105,7 +105,7 @@ aside { padding:16px; }h2 { margin:0 0 12px; color:var(--muted); font-size:11px;
|
||||||
.option-row { display:grid; grid-template-columns:1fr 1fr 1fr auto; gap:4px; align-items:center; padding:3px 0; border-bottom:1px solid var(--line); font-size:10px; }
|
.option-row { display:grid; grid-template-columns:1fr 1fr 1fr auto; gap:4px; align-items:center; padding:3px 0; border-bottom:1px solid var(--line); font-size:10px; }
|
||||||
.option-row.option-head { color:var(--muted); text-transform:uppercase; letter-spacing:.3px; font-size:8px; }
|
.option-row.option-head { color:var(--muted); text-transform:uppercase; letter-spacing:.3px; font-size:8px; }
|
||||||
.option-row button { padding:2px 6px; font-size:8px; }
|
.option-row button { padding:2px 6px; font-size:8px; }
|
||||||
.drawing-filters { display:grid; grid-template-columns:auto 1fr; gap:6px; margin-bottom:8px; }
|
.drawing-filters { display:grid; grid-template-columns:auto auto 1fr; gap:6px; margin-bottom:8px; }
|
||||||
.drawing-filters select, .drawing-filters input { font:inherit; font-size:10px; padding:4px 6px;
|
.drawing-filters select, .drawing-filters input { font:inherit; font-size:10px; padding:4px 6px;
|
||||||
border:1px solid var(--line); border-radius:5px; background:transparent; color:var(--fg); min-width:0; }
|
border:1px solid var(--line); border-radius:5px; background:transparent; color:var(--fg); min-width:0; }
|
||||||
.drawing-icon { display:flex; align-items:center; justify-content:center; color:var(--muted); font-size:10px; }
|
.drawing-icon { display:flex; align-items:center; justify-content:center; color:var(--muted); font-size:10px; }
|
||||||
|
|
|
||||||
|
|
@ -136,3 +136,27 @@ def test_every_drawing_gets_a_number_including_comments(tmp_path):
|
||||||
third = store.add(ManualLine(id="ml_c", slope=1.0, **common))
|
third = store.add(ManualLine(id="ml_c", slope=1.0, **common))
|
||||||
|
|
||||||
assert [first.number, note.number, third.number] == [1, 2, 3]
|
assert [first.number, note.number, third.number] == [1, 2, 3]
|
||||||
|
|
||||||
|
|
||||||
|
def test_a_null_cutoff_clears_an_ended_line(tmp_path):
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from app.api.routes import router
|
||||||
|
from app.config import Settings
|
||||||
|
from app.runtime import Runtime
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
app.include_router(router)
|
||||||
|
app.state.runtime = Runtime(Settings(manual_lines_path=tmp_path / "manual_lines.json"))
|
||||||
|
client = TestClient(app)
|
||||||
|
created = client.post("/api/lines", json={
|
||||||
|
"tf": "1m", "side": "support",
|
||||||
|
"anchor_t": 100, "anchor_p": 1.0,
|
||||||
|
"end_t": 200, "end_p": 2.0,
|
||||||
|
"cutoff_t": 150,
|
||||||
|
}).json()
|
||||||
|
assert created["cutoff_t"] == 150
|
||||||
|
|
||||||
|
cleared = client.patch(f"/api/lines/{created['id']}", json={"cutoff_t": None}).json()
|
||||||
|
assert cleared["cutoff_t"] is None
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue