chore: Fix .gitignore - remove test file exclusions, add coverage folder
- Removed incorrect exclusion of *.test.ts and *.test.js files - Added coverage/ folder to .gitignore - Removed accidentally committed coverage files Co-authored-by: mindesbunister <32161838+mindesbunister@users.noreply.github.com>
This commit is contained in:
9
.gitignore
vendored
9
.gitignore
vendored
@@ -31,12 +31,6 @@ logs/
|
||||
# Docker
|
||||
.dockerignore
|
||||
|
||||
# Test files
|
||||
*.test.ts
|
||||
*.test.js
|
||||
test-*.ts
|
||||
test-*.js
|
||||
|
||||
# Temporary files
|
||||
tmp/
|
||||
temp/
|
||||
@@ -45,3 +39,6 @@ temp/
|
||||
# Build artifacts
|
||||
dist/
|
||||
.backtester/
|
||||
|
||||
# Coverage reports
|
||||
coverage/
|
||||
|
||||
@@ -1,224 +0,0 @@
|
||||
body, html {
|
||||
margin:0; padding: 0;
|
||||
height: 100%;
|
||||
}
|
||||
body {
|
||||
font-family: Helvetica Neue, Helvetica, Arial;
|
||||
font-size: 14px;
|
||||
color:#333;
|
||||
}
|
||||
.small { font-size: 12px; }
|
||||
*, *:after, *:before {
|
||||
-webkit-box-sizing:border-box;
|
||||
-moz-box-sizing:border-box;
|
||||
box-sizing:border-box;
|
||||
}
|
||||
h1 { font-size: 20px; margin: 0;}
|
||||
h2 { font-size: 14px; }
|
||||
pre {
|
||||
font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-moz-tab-size: 2;
|
||||
-o-tab-size: 2;
|
||||
tab-size: 2;
|
||||
}
|
||||
a { color:#0074D9; text-decoration:none; }
|
||||
a:hover { text-decoration:underline; }
|
||||
.strong { font-weight: bold; }
|
||||
.space-top1 { padding: 10px 0 0 0; }
|
||||
.pad2y { padding: 20px 0; }
|
||||
.pad1y { padding: 10px 0; }
|
||||
.pad2x { padding: 0 20px; }
|
||||
.pad2 { padding: 20px; }
|
||||
.pad1 { padding: 10px; }
|
||||
.space-left2 { padding-left:55px; }
|
||||
.space-right2 { padding-right:20px; }
|
||||
.center { text-align:center; }
|
||||
.clearfix { display:block; }
|
||||
.clearfix:after {
|
||||
content:'';
|
||||
display:block;
|
||||
height:0;
|
||||
clear:both;
|
||||
visibility:hidden;
|
||||
}
|
||||
.fl { float: left; }
|
||||
@media only screen and (max-width:640px) {
|
||||
.col3 { width:100%; max-width:100%; }
|
||||
.hide-mobile { display:none!important; }
|
||||
}
|
||||
|
||||
.quiet {
|
||||
color: #7f7f7f;
|
||||
color: rgba(0,0,0,0.5);
|
||||
}
|
||||
.quiet a { opacity: 0.7; }
|
||||
|
||||
.fraction {
|
||||
font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace;
|
||||
font-size: 10px;
|
||||
color: #555;
|
||||
background: #E8E8E8;
|
||||
padding: 4px 5px;
|
||||
border-radius: 3px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
div.path a:link, div.path a:visited { color: #333; }
|
||||
table.coverage {
|
||||
border-collapse: collapse;
|
||||
margin: 10px 0 0 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
table.coverage td {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
vertical-align: top;
|
||||
}
|
||||
table.coverage td.line-count {
|
||||
text-align: right;
|
||||
padding: 0 5px 0 20px;
|
||||
}
|
||||
table.coverage td.line-coverage {
|
||||
text-align: right;
|
||||
padding-right: 10px;
|
||||
min-width:20px;
|
||||
}
|
||||
|
||||
table.coverage td span.cline-any {
|
||||
display: inline-block;
|
||||
padding: 0 5px;
|
||||
width: 100%;
|
||||
}
|
||||
.missing-if-branch {
|
||||
display: inline-block;
|
||||
margin-right: 5px;
|
||||
border-radius: 3px;
|
||||
position: relative;
|
||||
padding: 0 4px;
|
||||
background: #333;
|
||||
color: yellow;
|
||||
}
|
||||
|
||||
.skip-if-branch {
|
||||
display: none;
|
||||
margin-right: 10px;
|
||||
position: relative;
|
||||
padding: 0 4px;
|
||||
background: #ccc;
|
||||
color: white;
|
||||
}
|
||||
.missing-if-branch .typ, .skip-if-branch .typ {
|
||||
color: inherit !important;
|
||||
}
|
||||
.coverage-summary {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
}
|
||||
.coverage-summary tr { border-bottom: 1px solid #bbb; }
|
||||
.keyline-all { border: 1px solid #ddd; }
|
||||
.coverage-summary td, .coverage-summary th { padding: 10px; }
|
||||
.coverage-summary tbody { border: 1px solid #bbb; }
|
||||
.coverage-summary td { border-right: 1px solid #bbb; }
|
||||
.coverage-summary td:last-child { border-right: none; }
|
||||
.coverage-summary th {
|
||||
text-align: left;
|
||||
font-weight: normal;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.coverage-summary th.file { border-right: none !important; }
|
||||
.coverage-summary th.pct { }
|
||||
.coverage-summary th.pic,
|
||||
.coverage-summary th.abs,
|
||||
.coverage-summary td.pct,
|
||||
.coverage-summary td.abs { text-align: right; }
|
||||
.coverage-summary td.file { white-space: nowrap; }
|
||||
.coverage-summary td.pic { min-width: 120px !important; }
|
||||
.coverage-summary tfoot td { }
|
||||
|
||||
.coverage-summary .sorter {
|
||||
height: 10px;
|
||||
width: 7px;
|
||||
display: inline-block;
|
||||
margin-left: 0.5em;
|
||||
background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent;
|
||||
}
|
||||
.coverage-summary .sorted .sorter {
|
||||
background-position: 0 -20px;
|
||||
}
|
||||
.coverage-summary .sorted-desc .sorter {
|
||||
background-position: 0 -10px;
|
||||
}
|
||||
.status-line { height: 10px; }
|
||||
/* yellow */
|
||||
.cbranch-no { background: yellow !important; color: #111; }
|
||||
/* dark red */
|
||||
.red.solid, .status-line.low, .low .cover-fill { background:#C21F39 }
|
||||
.low .chart { border:1px solid #C21F39 }
|
||||
.highlighted,
|
||||
.highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{
|
||||
background: #C21F39 !important;
|
||||
}
|
||||
/* medium red */
|
||||
.cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE }
|
||||
/* light red */
|
||||
.low, .cline-no { background:#FCE1E5 }
|
||||
/* light green */
|
||||
.high, .cline-yes { background:rgb(230,245,208) }
|
||||
/* medium green */
|
||||
.cstat-yes { background:rgb(161,215,106) }
|
||||
/* dark green */
|
||||
.status-line.high, .high .cover-fill { background:rgb(77,146,33) }
|
||||
.high .chart { border:1px solid rgb(77,146,33) }
|
||||
/* dark yellow (gold) */
|
||||
.status-line.medium, .medium .cover-fill { background: #f9cd0b; }
|
||||
.medium .chart { border:1px solid #f9cd0b; }
|
||||
/* light yellow */
|
||||
.medium { background: #fff4c2; }
|
||||
|
||||
.cstat-skip { background: #ddd; color: #111; }
|
||||
.fstat-skip { background: #ddd; color: #111 !important; }
|
||||
.cbranch-skip { background: #ddd !important; color: #111; }
|
||||
|
||||
span.cline-neutral { background: #eaeaea; }
|
||||
|
||||
.coverage-summary td.empty {
|
||||
opacity: .5;
|
||||
padding-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
line-height: 1;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.cover-fill, .cover-empty {
|
||||
display:inline-block;
|
||||
height: 12px;
|
||||
}
|
||||
.chart {
|
||||
line-height: 0;
|
||||
}
|
||||
.cover-empty {
|
||||
background: white;
|
||||
}
|
||||
.cover-full {
|
||||
border-right: none !important;
|
||||
}
|
||||
pre.prettyprint {
|
||||
border: none !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
.com { color: #999 !important; }
|
||||
.ignore-none { color: #999; font-weight: normal; }
|
||||
|
||||
.wrapper {
|
||||
min-height: 100%;
|
||||
height: auto !important;
|
||||
height: 100%;
|
||||
margin: 0 auto -48px;
|
||||
}
|
||||
.footer, .push {
|
||||
height: 48px;
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
/* eslint-disable */
|
||||
var jumpToCode = (function init() {
|
||||
// Classes of code we would like to highlight in the file view
|
||||
var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no'];
|
||||
|
||||
// Elements to highlight in the file listing view
|
||||
var fileListingElements = ['td.pct.low'];
|
||||
|
||||
// We don't want to select elements that are direct descendants of another match
|
||||
var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > `
|
||||
|
||||
// Selector that finds elements on the page to which we can jump
|
||||
var selector =
|
||||
fileListingElements.join(', ') +
|
||||
', ' +
|
||||
notSelector +
|
||||
missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b`
|
||||
|
||||
// The NodeList of matching elements
|
||||
var missingCoverageElements = document.querySelectorAll(selector);
|
||||
|
||||
var currentIndex;
|
||||
|
||||
function toggleClass(index) {
|
||||
missingCoverageElements
|
||||
.item(currentIndex)
|
||||
.classList.remove('highlighted');
|
||||
missingCoverageElements.item(index).classList.add('highlighted');
|
||||
}
|
||||
|
||||
function makeCurrent(index) {
|
||||
toggleClass(index);
|
||||
currentIndex = index;
|
||||
missingCoverageElements.item(index).scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
inline: 'center'
|
||||
});
|
||||
}
|
||||
|
||||
function goToPrevious() {
|
||||
var nextIndex = 0;
|
||||
if (typeof currentIndex !== 'number' || currentIndex === 0) {
|
||||
nextIndex = missingCoverageElements.length - 1;
|
||||
} else if (missingCoverageElements.length > 1) {
|
||||
nextIndex = currentIndex - 1;
|
||||
}
|
||||
|
||||
makeCurrent(nextIndex);
|
||||
}
|
||||
|
||||
function goToNext() {
|
||||
var nextIndex = 0;
|
||||
|
||||
if (
|
||||
typeof currentIndex === 'number' &&
|
||||
currentIndex < missingCoverageElements.length - 1
|
||||
) {
|
||||
nextIndex = currentIndex + 1;
|
||||
}
|
||||
|
||||
makeCurrent(nextIndex);
|
||||
}
|
||||
|
||||
return function jump(event) {
|
||||
if (
|
||||
document.getElementById('fileSearch') === document.activeElement &&
|
||||
document.activeElement != null
|
||||
) {
|
||||
// if we're currently focused on the search input, we don't want to navigate
|
||||
return;
|
||||
}
|
||||
|
||||
switch (event.which) {
|
||||
case 78: // n
|
||||
case 74: // j
|
||||
goToNext();
|
||||
break;
|
||||
case 66: // b
|
||||
case 75: // k
|
||||
case 80: // p
|
||||
goToPrevious();
|
||||
break;
|
||||
}
|
||||
};
|
||||
})();
|
||||
window.addEventListener('keydown', jumpToCode);
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 445 B |
@@ -1,101 +0,0 @@
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>Code coverage report for All files</title>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="stylesheet" href="prettify.css" />
|
||||
<link rel="stylesheet" href="base.css" />
|
||||
<link rel="shortcut icon" type="image/x-icon" href="favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<style type='text/css'>
|
||||
.coverage-summary .sorter {
|
||||
background-image: url(sort-arrow-sprite.png);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class='wrapper'>
|
||||
<div class='pad1'>
|
||||
<h1>All files</h1>
|
||||
<div class='clearfix'>
|
||||
|
||||
<div class='fl pad1y space-right2'>
|
||||
<span class="strong">Unknown% </span>
|
||||
<span class="quiet">Statements</span>
|
||||
<span class='fraction'>0/0</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class='fl pad1y space-right2'>
|
||||
<span class="strong">Unknown% </span>
|
||||
<span class="quiet">Branches</span>
|
||||
<span class='fraction'>0/0</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class='fl pad1y space-right2'>
|
||||
<span class="strong">Unknown% </span>
|
||||
<span class="quiet">Functions</span>
|
||||
<span class='fraction'>0/0</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class='fl pad1y space-right2'>
|
||||
<span class="strong">Unknown% </span>
|
||||
<span class="quiet">Lines</span>
|
||||
<span class='fraction'>0/0</span>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
<p class="quiet">
|
||||
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
|
||||
</p>
|
||||
<template id="filterTemplate">
|
||||
<div class="quiet">
|
||||
Filter:
|
||||
<input type="search" id="fileSearch">
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class='status-line medium'></div>
|
||||
<div class="pad1">
|
||||
<table class="coverage-summary">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-col="file" data-fmt="html" data-html="true" class="file">File</th>
|
||||
<th data-col="pic" data-type="number" data-fmt="html" data-html="true" class="pic"></th>
|
||||
<th data-col="statements" data-type="number" data-fmt="pct" class="pct">Statements</th>
|
||||
<th data-col="statements_raw" data-type="number" data-fmt="html" class="abs"></th>
|
||||
<th data-col="branches" data-type="number" data-fmt="pct" class="pct">Branches</th>
|
||||
<th data-col="branches_raw" data-type="number" data-fmt="html" class="abs"></th>
|
||||
<th data-col="functions" data-type="number" data-fmt="pct" class="pct">Functions</th>
|
||||
<th data-col="functions_raw" data-type="number" data-fmt="html" class="abs"></th>
|
||||
<th data-col="lines" data-type="number" data-fmt="pct" class="pct">Lines</th>
|
||||
<th data-col="lines_raw" data-type="number" data-fmt="html" class="abs"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class='push'></div><!-- for sticky footer -->
|
||||
</div><!-- /wrapper -->
|
||||
<div class='footer quiet pad2 space-top1 center small'>
|
||||
Code coverage generated by
|
||||
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
|
||||
at 2025-12-05T00:13:47.160Z
|
||||
</div>
|
||||
<script src="prettify.js"></script>
|
||||
<script>
|
||||
window.onload = function () {
|
||||
prettyPrint();
|
||||
};
|
||||
</script>
|
||||
<script src="sorter.js"></script>
|
||||
<script src="block-navigation.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
.pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee}
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
|
Before Width: | Height: | Size: 138 B |
@@ -1,210 +0,0 @@
|
||||
/* eslint-disable */
|
||||
var addSorting = (function() {
|
||||
'use strict';
|
||||
var cols,
|
||||
currentSort = {
|
||||
index: 0,
|
||||
desc: false
|
||||
};
|
||||
|
||||
// returns the summary table element
|
||||
function getTable() {
|
||||
return document.querySelector('.coverage-summary');
|
||||
}
|
||||
// returns the thead element of the summary table
|
||||
function getTableHeader() {
|
||||
return getTable().querySelector('thead tr');
|
||||
}
|
||||
// returns the tbody element of the summary table
|
||||
function getTableBody() {
|
||||
return getTable().querySelector('tbody');
|
||||
}
|
||||
// returns the th element for nth column
|
||||
function getNthColumn(n) {
|
||||
return getTableHeader().querySelectorAll('th')[n];
|
||||
}
|
||||
|
||||
function onFilterInput() {
|
||||
const searchValue = document.getElementById('fileSearch').value;
|
||||
const rows = document.getElementsByTagName('tbody')[0].children;
|
||||
|
||||
// Try to create a RegExp from the searchValue. If it fails (invalid regex),
|
||||
// it will be treated as a plain text search
|
||||
let searchRegex;
|
||||
try {
|
||||
searchRegex = new RegExp(searchValue, 'i'); // 'i' for case-insensitive
|
||||
} catch (error) {
|
||||
searchRegex = null;
|
||||
}
|
||||
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const row = rows[i];
|
||||
let isMatch = false;
|
||||
|
||||
if (searchRegex) {
|
||||
// If a valid regex was created, use it for matching
|
||||
isMatch = searchRegex.test(row.textContent);
|
||||
} else {
|
||||
// Otherwise, fall back to the original plain text search
|
||||
isMatch = row.textContent
|
||||
.toLowerCase()
|
||||
.includes(searchValue.toLowerCase());
|
||||
}
|
||||
|
||||
row.style.display = isMatch ? '' : 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// loads the search box
|
||||
function addSearchBox() {
|
||||
var template = document.getElementById('filterTemplate');
|
||||
var templateClone = template.content.cloneNode(true);
|
||||
templateClone.getElementById('fileSearch').oninput = onFilterInput;
|
||||
template.parentElement.appendChild(templateClone);
|
||||
}
|
||||
|
||||
// loads all columns
|
||||
function loadColumns() {
|
||||
var colNodes = getTableHeader().querySelectorAll('th'),
|
||||
colNode,
|
||||
cols = [],
|
||||
col,
|
||||
i;
|
||||
|
||||
for (i = 0; i < colNodes.length; i += 1) {
|
||||
colNode = colNodes[i];
|
||||
col = {
|
||||
key: colNode.getAttribute('data-col'),
|
||||
sortable: !colNode.getAttribute('data-nosort'),
|
||||
type: colNode.getAttribute('data-type') || 'string'
|
||||
};
|
||||
cols.push(col);
|
||||
if (col.sortable) {
|
||||
col.defaultDescSort = col.type === 'number';
|
||||
colNode.innerHTML =
|
||||
colNode.innerHTML + '<span class="sorter"></span>';
|
||||
}
|
||||
}
|
||||
return cols;
|
||||
}
|
||||
// attaches a data attribute to every tr element with an object
|
||||
// of data values keyed by column name
|
||||
function loadRowData(tableRow) {
|
||||
var tableCols = tableRow.querySelectorAll('td'),
|
||||
colNode,
|
||||
col,
|
||||
data = {},
|
||||
i,
|
||||
val;
|
||||
for (i = 0; i < tableCols.length; i += 1) {
|
||||
colNode = tableCols[i];
|
||||
col = cols[i];
|
||||
val = colNode.getAttribute('data-value');
|
||||
if (col.type === 'number') {
|
||||
val = Number(val);
|
||||
}
|
||||
data[col.key] = val;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
// loads all row data
|
||||
function loadData() {
|
||||
var rows = getTableBody().querySelectorAll('tr'),
|
||||
i;
|
||||
|
||||
for (i = 0; i < rows.length; i += 1) {
|
||||
rows[i].data = loadRowData(rows[i]);
|
||||
}
|
||||
}
|
||||
// sorts the table using the data for the ith column
|
||||
function sortByIndex(index, desc) {
|
||||
var key = cols[index].key,
|
||||
sorter = function(a, b) {
|
||||
a = a.data[key];
|
||||
b = b.data[key];
|
||||
return a < b ? -1 : a > b ? 1 : 0;
|
||||
},
|
||||
finalSorter = sorter,
|
||||
tableBody = document.querySelector('.coverage-summary tbody'),
|
||||
rowNodes = tableBody.querySelectorAll('tr'),
|
||||
rows = [],
|
||||
i;
|
||||
|
||||
if (desc) {
|
||||
finalSorter = function(a, b) {
|
||||
return -1 * sorter(a, b);
|
||||
};
|
||||
}
|
||||
|
||||
for (i = 0; i < rowNodes.length; i += 1) {
|
||||
rows.push(rowNodes[i]);
|
||||
tableBody.removeChild(rowNodes[i]);
|
||||
}
|
||||
|
||||
rows.sort(finalSorter);
|
||||
|
||||
for (i = 0; i < rows.length; i += 1) {
|
||||
tableBody.appendChild(rows[i]);
|
||||
}
|
||||
}
|
||||
// removes sort indicators for current column being sorted
|
||||
function removeSortIndicators() {
|
||||
var col = getNthColumn(currentSort.index),
|
||||
cls = col.className;
|
||||
|
||||
cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, '');
|
||||
col.className = cls;
|
||||
}
|
||||
// adds sort indicators for current column being sorted
|
||||
function addSortIndicators() {
|
||||
getNthColumn(currentSort.index).className += currentSort.desc
|
||||
? ' sorted-desc'
|
||||
: ' sorted';
|
||||
}
|
||||
// adds event listeners for all sorter widgets
|
||||
function enableUI() {
|
||||
var i,
|
||||
el,
|
||||
ithSorter = function ithSorter(i) {
|
||||
var col = cols[i];
|
||||
|
||||
return function() {
|
||||
var desc = col.defaultDescSort;
|
||||
|
||||
if (currentSort.index === i) {
|
||||
desc = !currentSort.desc;
|
||||
}
|
||||
sortByIndex(i, desc);
|
||||
removeSortIndicators();
|
||||
currentSort.index = i;
|
||||
currentSort.desc = desc;
|
||||
addSortIndicators();
|
||||
};
|
||||
};
|
||||
for (i = 0; i < cols.length; i += 1) {
|
||||
if (cols[i].sortable) {
|
||||
// add the click event handler on the th so users
|
||||
// dont have to click on those tiny arrows
|
||||
el = getNthColumn(i).querySelector('.sorter').parentElement;
|
||||
if (el.addEventListener) {
|
||||
el.addEventListener('click', ithSorter(i));
|
||||
} else {
|
||||
el.attachEvent('onclick', ithSorter(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// adds sorting functionality to the UI
|
||||
return function() {
|
||||
if (!getTable()) {
|
||||
return;
|
||||
}
|
||||
cols = loadColumns();
|
||||
loadData();
|
||||
addSearchBox();
|
||||
addSortIndicators();
|
||||
enableUI();
|
||||
};
|
||||
})();
|
||||
|
||||
window.addEventListener('load', addSorting);
|
||||
200
tests/integration/position-manager/adx-runner-sl.test.ts
Normal file
200
tests/integration/position-manager/adx-runner-sl.test.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
/**
|
||||
* ADX-Based Runner Stop Loss Tests
|
||||
*
|
||||
* Tests for ADX-based runner SL positioning after TP1 (Pitfall #52).
|
||||
*
|
||||
* Runner SL tiers based on ADX at entry:
|
||||
* - ADX < 20: SL at 0% (breakeven) - Weak trend, preserve capital
|
||||
* - ADX 20-25: SL at -0.3% - Moderate trend, some room
|
||||
* - ADX > 25: SL at -0.55% - Strong trend, full retracement room
|
||||
*/
|
||||
|
||||
import {
|
||||
createLongTrade,
|
||||
createShortTrade,
|
||||
TEST_DEFAULTS,
|
||||
calculateTargetPrice
|
||||
} from '../../helpers/trade-factory'
|
||||
|
||||
describe('ADX-Based Runner Stop Loss', () => {
|
||||
// Extract the ADX-based runner SL logic from Position Manager
|
||||
function calculateRunnerSLPercent(adx: number): number {
|
||||
if (adx < 20) {
|
||||
return 0 // Weak trend: breakeven, preserve capital
|
||||
} else if (adx < 25) {
|
||||
return -0.3 // Moderate trend: some room
|
||||
} else {
|
||||
return -0.55 // Strong trend: full retracement room
|
||||
}
|
||||
}
|
||||
|
||||
function calculatePrice(
|
||||
entryPrice: number,
|
||||
percent: number,
|
||||
direction: 'long' | 'short'
|
||||
): number {
|
||||
if (direction === 'long') {
|
||||
return entryPrice * (1 + percent / 100)
|
||||
} else {
|
||||
return entryPrice * (1 - percent / 100)
|
||||
}
|
||||
}
|
||||
|
||||
describe('ADX < 20: Weak trend - breakeven SL', () => {
|
||||
it('should return 0% SL for ADX 15 (weak trend)', () => {
|
||||
const runnerSlPercent = calculateRunnerSLPercent(15)
|
||||
expect(runnerSlPercent).toBe(0)
|
||||
})
|
||||
|
||||
it('should return 0% SL for ADX 19.9 (boundary)', () => {
|
||||
const runnerSlPercent = calculateRunnerSLPercent(19.9)
|
||||
expect(runnerSlPercent).toBe(0)
|
||||
})
|
||||
|
||||
it('LONG: should set runner SL at entry price for ADX 18', () => {
|
||||
const trade = createLongTrade({ adx: 18, entryPrice: 140 })
|
||||
const runnerSlPercent = calculateRunnerSLPercent(trade.adxAtEntry!)
|
||||
const runnerSL = calculatePrice(trade.entryPrice, runnerSlPercent, 'long')
|
||||
|
||||
expect(runnerSlPercent).toBe(0)
|
||||
expect(runnerSL).toBe(140.00) // Breakeven
|
||||
})
|
||||
|
||||
it('SHORT: should set runner SL at entry price for ADX 18', () => {
|
||||
const trade = createShortTrade({ adx: 18, entryPrice: 140 })
|
||||
const runnerSlPercent = calculateRunnerSLPercent(trade.adxAtEntry!)
|
||||
const runnerSL = calculatePrice(trade.entryPrice, runnerSlPercent, 'short')
|
||||
|
||||
expect(runnerSlPercent).toBe(0)
|
||||
expect(runnerSL).toBe(140.00) // Breakeven
|
||||
})
|
||||
})
|
||||
|
||||
describe('ADX 20-25: Moderate trend - -0.3% SL', () => {
|
||||
it('should return -0.3% SL for ADX 20 (boundary)', () => {
|
||||
const runnerSlPercent = calculateRunnerSLPercent(20)
|
||||
expect(runnerSlPercent).toBe(-0.3)
|
||||
})
|
||||
|
||||
it('should return -0.3% SL for ADX 22', () => {
|
||||
const runnerSlPercent = calculateRunnerSLPercent(22)
|
||||
expect(runnerSlPercent).toBe(-0.3)
|
||||
})
|
||||
|
||||
it('should return -0.3% SL for ADX 24.9 (boundary)', () => {
|
||||
const runnerSlPercent = calculateRunnerSLPercent(24.9)
|
||||
expect(runnerSlPercent).toBe(-0.3)
|
||||
})
|
||||
|
||||
it('LONG: should set runner SL at -0.3% below entry for ADX 22', () => {
|
||||
const trade = createLongTrade({ adx: 22, entryPrice: 140 })
|
||||
const runnerSlPercent = calculateRunnerSLPercent(trade.adxAtEntry!)
|
||||
const runnerSL = calculatePrice(trade.entryPrice, runnerSlPercent, 'long')
|
||||
|
||||
expect(runnerSlPercent).toBe(-0.3)
|
||||
expect(runnerSL).toBeCloseTo(139.58, 2) // 140 * (1 - 0.003) = 139.58
|
||||
expect(runnerSL).toBeLessThan(trade.entryPrice)
|
||||
})
|
||||
|
||||
it('SHORT: should set runner SL at -0.3% above entry for ADX 22', () => {
|
||||
const trade = createShortTrade({ adx: 22, entryPrice: 140 })
|
||||
const runnerSlPercent = calculateRunnerSLPercent(trade.adxAtEntry!)
|
||||
const runnerSL = calculatePrice(trade.entryPrice, runnerSlPercent, 'short')
|
||||
|
||||
expect(runnerSlPercent).toBe(-0.3)
|
||||
expect(runnerSL).toBeCloseTo(140.42, 2) // 140 * (1 + 0.003) = 140.42
|
||||
expect(runnerSL).toBeGreaterThan(trade.entryPrice)
|
||||
})
|
||||
})
|
||||
|
||||
describe('ADX > 25: Strong trend - -0.55% SL', () => {
|
||||
it('should return -0.55% SL for ADX 25 (boundary)', () => {
|
||||
const runnerSlPercent = calculateRunnerSLPercent(25)
|
||||
expect(runnerSlPercent).toBe(-0.55)
|
||||
})
|
||||
|
||||
it('should return -0.55% SL for ADX 26.9 (test default)', () => {
|
||||
const runnerSlPercent = calculateRunnerSLPercent(TEST_DEFAULTS.adx)
|
||||
expect(runnerSlPercent).toBe(-0.55)
|
||||
})
|
||||
|
||||
it('should return -0.55% SL for ADX 35 (very strong)', () => {
|
||||
const runnerSlPercent = calculateRunnerSLPercent(35)
|
||||
expect(runnerSlPercent).toBe(-0.55)
|
||||
})
|
||||
|
||||
it('LONG: should set runner SL at -0.55% below entry for ADX 26.9', () => {
|
||||
const trade = createLongTrade({ adx: 26.9, entryPrice: 140 })
|
||||
const runnerSlPercent = calculateRunnerSLPercent(trade.adxAtEntry!)
|
||||
const runnerSL = calculatePrice(trade.entryPrice, runnerSlPercent, 'long')
|
||||
|
||||
expect(runnerSlPercent).toBe(-0.55)
|
||||
expect(runnerSL).toBeCloseTo(139.23, 2) // 140 * (1 - 0.0055) = 139.23
|
||||
expect(runnerSL).toBeLessThan(trade.entryPrice)
|
||||
})
|
||||
|
||||
it('SHORT: should set runner SL at -0.55% above entry for ADX 26.9', () => {
|
||||
const trade = createShortTrade({ adx: 26.9, entryPrice: 140 })
|
||||
const runnerSlPercent = calculateRunnerSLPercent(trade.adxAtEntry!)
|
||||
const runnerSL = calculatePrice(trade.entryPrice, runnerSlPercent, 'short')
|
||||
|
||||
expect(runnerSlPercent).toBe(-0.55)
|
||||
expect(runnerSL).toBeCloseTo(140.77, 2) // 140 * (1 + 0.0055) = 140.77
|
||||
expect(runnerSL).toBeGreaterThan(trade.entryPrice)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Missing ADX handling', () => {
|
||||
it('should default to 0% (breakeven) when ADX is 0', () => {
|
||||
const runnerSlPercent = calculateRunnerSLPercent(0)
|
||||
expect(runnerSlPercent).toBe(0) // Conservative default
|
||||
})
|
||||
|
||||
it('should handle trades with no ADX data', () => {
|
||||
// When ADX is undefined, the Position Manager uses 0 as default
|
||||
// According to the logic: ADX < 20 = 0% SL (breakeven)
|
||||
const adx = undefined
|
||||
const adxValue = adx || 0
|
||||
const runnerSlPercent = calculateRunnerSLPercent(adxValue)
|
||||
|
||||
// No ADX = 0 = weak trend = breakeven
|
||||
expect(runnerSlPercent).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Retracement room validation', () => {
|
||||
it('LONG ADX 26.9: runner can handle -0.55% retracement without stop', () => {
|
||||
const entryPrice = 140
|
||||
const runnerSL = calculatePrice(entryPrice, -0.55, 'long')
|
||||
|
||||
// Price at -0.4% should NOT hit SL
|
||||
const priceAt0_4PercentDrop = entryPrice * 0.996 // 139.44
|
||||
expect(priceAt0_4PercentDrop).toBeGreaterThan(runnerSL)
|
||||
|
||||
// Price at -0.55% should hit SL
|
||||
const priceAt0_55PercentDrop = runnerSL
|
||||
expect(priceAt0_55PercentDrop).toBe(runnerSL)
|
||||
|
||||
// Price at -0.6% should definitely hit SL
|
||||
const priceAt0_6PercentDrop = entryPrice * 0.994 // 139.16
|
||||
expect(priceAt0_6PercentDrop).toBeLessThan(runnerSL)
|
||||
})
|
||||
|
||||
it('SHORT ADX 26.9: runner can handle +0.55% retracement without stop', () => {
|
||||
const entryPrice = 140
|
||||
const runnerSL = calculatePrice(entryPrice, -0.55, 'short')
|
||||
|
||||
// Price at +0.4% should NOT hit SL
|
||||
const priceAt0_4PercentRise = entryPrice * 1.004 // 140.56
|
||||
expect(priceAt0_4PercentRise).toBeLessThan(runnerSL)
|
||||
|
||||
// Price at +0.55% should hit SL
|
||||
const priceAt0_55PercentRise = runnerSL
|
||||
expect(priceAt0_55PercentRise).toBe(runnerSL)
|
||||
|
||||
// Price at +0.6% should definitely hit SL
|
||||
const priceAt0_6PercentRise = entryPrice * 1.006 // 140.84
|
||||
expect(priceAt0_6PercentRise).toBeGreaterThan(runnerSL)
|
||||
})
|
||||
})
|
||||
})
|
||||
155
tests/integration/position-manager/breakeven-sl.test.ts
Normal file
155
tests/integration/position-manager/breakeven-sl.test.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
/**
|
||||
* Breakeven Stop Loss Tests
|
||||
*
|
||||
* Tests for SL movement to breakeven after TP1 hit.
|
||||
*
|
||||
* Key behaviors tested:
|
||||
* - SL moves to entry price (breakeven) after TP1 for LONG
|
||||
* - SL moves to entry price (breakeven) after TP1 for SHORT
|
||||
* - CRITICAL (Pitfall #45): Must use DATABASE entry price, not Drift recalculated entry
|
||||
*/
|
||||
|
||||
import {
|
||||
createLongTrade,
|
||||
createShortTrade,
|
||||
createTradeAfterTP1,
|
||||
TEST_DEFAULTS,
|
||||
calculateTargetPrice
|
||||
} from '../../helpers/trade-factory'
|
||||
|
||||
describe('Breakeven Stop Loss', () => {
|
||||
// Test the calculatePrice logic extracted from Position Manager
|
||||
function calculatePrice(
|
||||
entryPrice: number,
|
||||
percent: number,
|
||||
direction: 'long' | 'short'
|
||||
): number {
|
||||
if (direction === 'long') {
|
||||
return entryPrice * (1 + percent / 100)
|
||||
} else {
|
||||
return entryPrice * (1 - percent / 100)
|
||||
}
|
||||
}
|
||||
|
||||
describe('LONG positions after TP1', () => {
|
||||
it('should calculate breakeven SL at entry price for LONG', () => {
|
||||
const trade = createLongTrade({ entryPrice: 140.00 })
|
||||
|
||||
// After TP1, SL moves to breakeven (0%)
|
||||
const breakevenSL = calculatePrice(trade.entryPrice, 0, 'long')
|
||||
|
||||
expect(breakevenSL).toBe(140.00)
|
||||
})
|
||||
|
||||
it('should protect 60% profit when runner SL at breakeven', () => {
|
||||
// After TP1 closes 60%, remaining 40% has SL at entry
|
||||
const trade = createTradeAfterTP1('long')
|
||||
|
||||
expect(trade.tp1Hit).toBe(true)
|
||||
expect(trade.slMovedToBreakeven).toBe(true)
|
||||
expect(trade.stopLossPrice).toBe(TEST_DEFAULTS.entry)
|
||||
|
||||
// Runner size should be 40% of original
|
||||
expect(trade.currentSize).toBe(TEST_DEFAULTS.positionSize * 0.4)
|
||||
})
|
||||
|
||||
it('should use DATABASE entry price, NOT Drift recalculated entry (Pitfall #45)', () => {
|
||||
// CRITICAL: After partial close, Drift recalculates entry price based on remaining position
|
||||
// This would give wrong breakeven SL. Must use ORIGINAL entry from database.
|
||||
|
||||
const databaseEntryPrice = 140.00
|
||||
const driftRecalculatedEntry = 140.50 // Wrong! Drift adjusts after partial close
|
||||
|
||||
// Correct: Use database entry
|
||||
const correctBreakevenSL = calculatePrice(databaseEntryPrice, 0, 'long')
|
||||
expect(correctBreakevenSL).toBe(140.00)
|
||||
|
||||
// Wrong: Using Drift entry would give incorrect SL
|
||||
const wrongBreakevenSL = calculatePrice(driftRecalculatedEntry, 0, 'long')
|
||||
expect(wrongBreakevenSL).not.toBe(140.00)
|
||||
expect(wrongBreakevenSL).toBe(140.50) // This would be wrong!
|
||||
|
||||
// The trade factory correctly uses original entry
|
||||
const trade = createTradeAfterTP1('long', { entryPrice: databaseEntryPrice })
|
||||
expect(trade.entryPrice).toBe(databaseEntryPrice)
|
||||
expect(trade.stopLossPrice).toBe(databaseEntryPrice)
|
||||
})
|
||||
})
|
||||
|
||||
describe('SHORT positions after TP1', () => {
|
||||
it('should calculate breakeven SL at entry price for SHORT', () => {
|
||||
const trade = createShortTrade({ entryPrice: 140.00 })
|
||||
|
||||
// After TP1, SL moves to breakeven (0%)
|
||||
const breakevenSL = calculatePrice(trade.entryPrice, 0, 'short')
|
||||
|
||||
expect(breakevenSL).toBe(140.00)
|
||||
})
|
||||
|
||||
it('should protect 60% profit when runner SL at breakeven', () => {
|
||||
const trade = createTradeAfterTP1('short')
|
||||
|
||||
expect(trade.tp1Hit).toBe(true)
|
||||
expect(trade.slMovedToBreakeven).toBe(true)
|
||||
expect(trade.stopLossPrice).toBe(TEST_DEFAULTS.entry)
|
||||
|
||||
// Runner size should be 40% of original
|
||||
expect(trade.currentSize).toBe(TEST_DEFAULTS.positionSize * 0.4)
|
||||
})
|
||||
|
||||
it('should use DATABASE entry price, NOT Drift recalculated entry (Pitfall #45)', () => {
|
||||
const databaseEntryPrice = 140.00
|
||||
const driftRecalculatedEntry = 139.50 // Wrong! Drift adjusts after partial close
|
||||
|
||||
// Correct: Use database entry
|
||||
const correctBreakevenSL = calculatePrice(databaseEntryPrice, 0, 'short')
|
||||
expect(correctBreakevenSL).toBe(140.00)
|
||||
|
||||
// Wrong: Using Drift entry would give incorrect SL
|
||||
const wrongBreakevenSL = calculatePrice(driftRecalculatedEntry, 0, 'short')
|
||||
expect(wrongBreakevenSL).not.toBe(140.00)
|
||||
|
||||
// The trade factory correctly uses original entry
|
||||
const trade = createTradeAfterTP1('short', { entryPrice: databaseEntryPrice })
|
||||
expect(trade.entryPrice).toBe(databaseEntryPrice)
|
||||
expect(trade.stopLossPrice).toBe(databaseEntryPrice)
|
||||
})
|
||||
})
|
||||
|
||||
describe('SL direction verification', () => {
|
||||
it('LONG: breakeven SL should be BELOW entry when negative %', () => {
|
||||
const entryPrice = 140.00
|
||||
|
||||
// For LONG: negative % = lower price = valid SL
|
||||
const slAt0_3PercentLoss = calculatePrice(entryPrice, -0.3, 'long')
|
||||
expect(slAt0_3PercentLoss).toBe(139.58) // 140 * (1 - 0.003)
|
||||
expect(slAt0_3PercentLoss).toBeLessThan(entryPrice)
|
||||
})
|
||||
|
||||
it('SHORT: breakeven SL should be ABOVE entry when negative %', () => {
|
||||
const entryPrice = 140.00
|
||||
|
||||
// For SHORT: negative % = higher price = valid SL
|
||||
const slAt0_3PercentLoss = calculatePrice(entryPrice, -0.3, 'short')
|
||||
expect(slAt0_3PercentLoss).toBe(140.42) // 140 * (1 + 0.003)
|
||||
expect(slAt0_3PercentLoss).toBeGreaterThan(entryPrice)
|
||||
})
|
||||
|
||||
it('should verify SL moves in profitable direction', () => {
|
||||
const longTrade = createLongTrade({ entryPrice: 140 })
|
||||
const shortTrade = createShortTrade({ entryPrice: 140 })
|
||||
|
||||
// Original SLs are at loss levels
|
||||
expect(longTrade.stopLossPrice).toBe(TEST_DEFAULTS.long.sl) // Below entry
|
||||
expect(shortTrade.stopLossPrice).toBe(TEST_DEFAULTS.short.sl) // Above entry
|
||||
|
||||
// After TP1, SLs move to breakeven (entry price)
|
||||
const longAfterTP1 = createTradeAfterTP1('long')
|
||||
const shortAfterTP1 = createTradeAfterTP1('short')
|
||||
|
||||
// Both should now be at entry price
|
||||
expect(longAfterTP1.stopLossPrice).toBe(TEST_DEFAULTS.entry)
|
||||
expect(shortAfterTP1.stopLossPrice).toBe(TEST_DEFAULTS.entry)
|
||||
})
|
||||
})
|
||||
})
|
||||
308
tests/integration/position-manager/decision-helpers.test.ts
Normal file
308
tests/integration/position-manager/decision-helpers.test.ts
Normal file
@@ -0,0 +1,308 @@
|
||||
/**
|
||||
* Decision Helpers Tests
|
||||
*
|
||||
* Tests for all decision helper functions in Position Manager:
|
||||
* - shouldEmergencyStop() - LONG and SHORT
|
||||
* - shouldStopLoss() - LONG and SHORT
|
||||
* - shouldTakeProfit1() - LONG and SHORT
|
||||
* - shouldTakeProfit2() - LONG and SHORT
|
||||
*/
|
||||
|
||||
import {
|
||||
createLongTrade,
|
||||
createShortTrade,
|
||||
TEST_DEFAULTS
|
||||
} from '../../helpers/trade-factory'
|
||||
|
||||
describe('Decision Helpers', () => {
|
||||
// Extract decision helpers from Position Manager
|
||||
// These are pure functions that test price against targets
|
||||
|
||||
function shouldEmergencyStop(price: number, trade: { direction: 'long' | 'short', emergencyStopPrice: number }): boolean {
|
||||
if (trade.direction === 'long') {
|
||||
return price <= trade.emergencyStopPrice
|
||||
} else {
|
||||
return price >= trade.emergencyStopPrice
|
||||
}
|
||||
}
|
||||
|
||||
function shouldStopLoss(price: number, trade: { direction: 'long' | 'short', stopLossPrice: number }): boolean {
|
||||
if (trade.direction === 'long') {
|
||||
return price <= trade.stopLossPrice
|
||||
} else {
|
||||
return price >= trade.stopLossPrice
|
||||
}
|
||||
}
|
||||
|
||||
function shouldTakeProfit1(price: number, trade: { direction: 'long' | 'short', tp1Price: number }): boolean {
|
||||
if (trade.direction === 'long') {
|
||||
return price >= trade.tp1Price
|
||||
} else {
|
||||
return price <= trade.tp1Price
|
||||
}
|
||||
}
|
||||
|
||||
function shouldTakeProfit2(price: number, trade: { direction: 'long' | 'short', tp2Price: number }): boolean {
|
||||
if (trade.direction === 'long') {
|
||||
return price >= trade.tp2Price
|
||||
} else {
|
||||
return price <= trade.tp2Price
|
||||
}
|
||||
}
|
||||
|
||||
describe('shouldEmergencyStop()', () => {
|
||||
describe('LONG positions', () => {
|
||||
it('should trigger emergency stop when price at -2% (emergency level)', () => {
|
||||
const trade = createLongTrade({ entryPrice: 140 })
|
||||
// Emergency stop at -2% = $137.20
|
||||
expect(trade.emergencyStopPrice).toBe(TEST_DEFAULTS.long.emergencySl)
|
||||
|
||||
expect(shouldEmergencyStop(137.20, trade)).toBe(true)
|
||||
})
|
||||
|
||||
it('should trigger emergency stop when price below emergency level', () => {
|
||||
const trade = createLongTrade()
|
||||
|
||||
expect(shouldEmergencyStop(136.00, trade)).toBe(true) // Below emergency
|
||||
expect(shouldEmergencyStop(135.00, trade)).toBe(true) // Well below
|
||||
})
|
||||
|
||||
it('should NOT trigger emergency stop above emergency level', () => {
|
||||
const trade = createLongTrade()
|
||||
|
||||
expect(shouldEmergencyStop(138.00, trade)).toBe(false) // Above emergency but below SL
|
||||
expect(shouldEmergencyStop(140.00, trade)).toBe(false) // At entry
|
||||
expect(shouldEmergencyStop(141.00, trade)).toBe(false) // In profit
|
||||
})
|
||||
})
|
||||
|
||||
describe('SHORT positions', () => {
|
||||
it('should trigger emergency stop when price at +2% (emergency level)', () => {
|
||||
const trade = createShortTrade({ entryPrice: 140 })
|
||||
// Emergency stop at +2% = $142.80
|
||||
expect(trade.emergencyStopPrice).toBe(TEST_DEFAULTS.short.emergencySl)
|
||||
|
||||
expect(shouldEmergencyStop(142.80, trade)).toBe(true)
|
||||
})
|
||||
|
||||
it('should trigger emergency stop when price above emergency level', () => {
|
||||
const trade = createShortTrade()
|
||||
|
||||
expect(shouldEmergencyStop(143.00, trade)).toBe(true)
|
||||
expect(shouldEmergencyStop(145.00, trade)).toBe(true)
|
||||
})
|
||||
|
||||
it('should NOT trigger emergency stop below emergency level', () => {
|
||||
const trade = createShortTrade()
|
||||
|
||||
expect(shouldEmergencyStop(142.00, trade)).toBe(false) // Below emergency but at SL
|
||||
expect(shouldEmergencyStop(140.00, trade)).toBe(false) // At entry
|
||||
expect(shouldEmergencyStop(138.00, trade)).toBe(false) // In profit
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('shouldStopLoss()', () => {
|
||||
describe('LONG positions', () => {
|
||||
it('should trigger SL when price at stop loss level', () => {
|
||||
const trade = createLongTrade({ entryPrice: 140 })
|
||||
// SL at -0.92% = $138.71
|
||||
expect(trade.stopLossPrice).toBe(TEST_DEFAULTS.long.sl)
|
||||
|
||||
expect(shouldStopLoss(138.71, trade)).toBe(true)
|
||||
})
|
||||
|
||||
it('should trigger SL when price below stop loss', () => {
|
||||
const trade = createLongTrade()
|
||||
|
||||
expect(shouldStopLoss(138.00, trade)).toBe(true)
|
||||
expect(shouldStopLoss(137.50, trade)).toBe(true)
|
||||
})
|
||||
|
||||
it('should NOT trigger SL when price above stop loss', () => {
|
||||
const trade = createLongTrade()
|
||||
|
||||
expect(shouldStopLoss(139.00, trade)).toBe(false)
|
||||
expect(shouldStopLoss(140.00, trade)).toBe(false)
|
||||
expect(shouldStopLoss(141.00, trade)).toBe(false)
|
||||
})
|
||||
|
||||
it('should work with adjusted SL (breakeven after TP1)', () => {
|
||||
const trade = createLongTrade({
|
||||
entryPrice: 140,
|
||||
stopLossPrice: 140.00, // Moved to breakeven
|
||||
tp1Hit: true
|
||||
})
|
||||
|
||||
expect(shouldStopLoss(139.99, trade)).toBe(true) // Just below breakeven
|
||||
expect(shouldStopLoss(140.00, trade)).toBe(true) // At breakeven
|
||||
expect(shouldStopLoss(140.01, trade)).toBe(false) // Above breakeven
|
||||
})
|
||||
})
|
||||
|
||||
describe('SHORT positions', () => {
|
||||
it('should trigger SL when price at stop loss level', () => {
|
||||
const trade = createShortTrade({ entryPrice: 140 })
|
||||
// SL at +0.92% = $141.29
|
||||
expect(trade.stopLossPrice).toBe(TEST_DEFAULTS.short.sl)
|
||||
|
||||
expect(shouldStopLoss(141.29, trade)).toBe(true)
|
||||
})
|
||||
|
||||
it('should trigger SL when price above stop loss', () => {
|
||||
const trade = createShortTrade()
|
||||
|
||||
expect(shouldStopLoss(142.00, trade)).toBe(true)
|
||||
expect(shouldStopLoss(143.00, trade)).toBe(true)
|
||||
})
|
||||
|
||||
it('should NOT trigger SL when price below stop loss', () => {
|
||||
const trade = createShortTrade()
|
||||
|
||||
expect(shouldStopLoss(141.00, trade)).toBe(false)
|
||||
expect(shouldStopLoss(140.00, trade)).toBe(false)
|
||||
expect(shouldStopLoss(138.00, trade)).toBe(false)
|
||||
})
|
||||
|
||||
it('should work with adjusted SL (breakeven after TP1)', () => {
|
||||
const trade = createShortTrade({
|
||||
entryPrice: 140,
|
||||
stopLossPrice: 140.00, // Moved to breakeven
|
||||
tp1Hit: true
|
||||
})
|
||||
|
||||
expect(shouldStopLoss(140.01, trade)).toBe(true) // Just above breakeven
|
||||
expect(shouldStopLoss(140.00, trade)).toBe(true) // At breakeven
|
||||
expect(shouldStopLoss(139.99, trade)).toBe(false) // Below breakeven (in profit)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('shouldTakeProfit1()', () => {
|
||||
describe('LONG positions', () => {
|
||||
it('should trigger TP1 when price at target', () => {
|
||||
const trade = createLongTrade({ entryPrice: 140 })
|
||||
expect(trade.tp1Price).toBe(TEST_DEFAULTS.long.tp1)
|
||||
|
||||
expect(shouldTakeProfit1(141.20, trade)).toBe(true)
|
||||
})
|
||||
|
||||
it('should trigger TP1 when price above target', () => {
|
||||
const trade = createLongTrade()
|
||||
|
||||
expect(shouldTakeProfit1(141.50, trade)).toBe(true)
|
||||
expect(shouldTakeProfit1(142.00, trade)).toBe(true)
|
||||
})
|
||||
|
||||
it('should NOT trigger TP1 when price below target', () => {
|
||||
const trade = createLongTrade()
|
||||
|
||||
expect(shouldTakeProfit1(141.00, trade)).toBe(false)
|
||||
expect(shouldTakeProfit1(140.00, trade)).toBe(false)
|
||||
expect(shouldTakeProfit1(139.00, trade)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('SHORT positions', () => {
|
||||
it('should trigger TP1 when price at target', () => {
|
||||
const trade = createShortTrade({ entryPrice: 140 })
|
||||
expect(trade.tp1Price).toBe(TEST_DEFAULTS.short.tp1)
|
||||
|
||||
expect(shouldTakeProfit1(138.80, trade)).toBe(true)
|
||||
})
|
||||
|
||||
it('should trigger TP1 when price below target (better for short)', () => {
|
||||
const trade = createShortTrade()
|
||||
|
||||
expect(shouldTakeProfit1(138.50, trade)).toBe(true)
|
||||
expect(shouldTakeProfit1(138.00, trade)).toBe(true)
|
||||
})
|
||||
|
||||
it('should NOT trigger TP1 when price above target', () => {
|
||||
const trade = createShortTrade()
|
||||
|
||||
expect(shouldTakeProfit1(139.00, trade)).toBe(false)
|
||||
expect(shouldTakeProfit1(140.00, trade)).toBe(false)
|
||||
expect(shouldTakeProfit1(141.00, trade)).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('shouldTakeProfit2()', () => {
|
||||
describe('LONG positions', () => {
|
||||
it('should trigger TP2 when price at target', () => {
|
||||
const trade = createLongTrade({ entryPrice: 140 })
|
||||
expect(trade.tp2Price).toBe(TEST_DEFAULTS.long.tp2)
|
||||
|
||||
expect(shouldTakeProfit2(142.41, trade)).toBe(true)
|
||||
})
|
||||
|
||||
it('should trigger TP2 when price above target', () => {
|
||||
const trade = createLongTrade()
|
||||
|
||||
expect(shouldTakeProfit2(143.00, trade)).toBe(true)
|
||||
expect(shouldTakeProfit2(145.00, trade)).toBe(true)
|
||||
})
|
||||
|
||||
it('should NOT trigger TP2 when price below target', () => {
|
||||
const trade = createLongTrade()
|
||||
|
||||
expect(shouldTakeProfit2(142.00, trade)).toBe(false)
|
||||
expect(shouldTakeProfit2(141.00, trade)).toBe(false)
|
||||
expect(shouldTakeProfit2(140.00, trade)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('SHORT positions', () => {
|
||||
it('should trigger TP2 when price at target', () => {
|
||||
const trade = createShortTrade({ entryPrice: 140 })
|
||||
expect(trade.tp2Price).toBe(TEST_DEFAULTS.short.tp2)
|
||||
|
||||
expect(shouldTakeProfit2(137.59, trade)).toBe(true)
|
||||
})
|
||||
|
||||
it('should trigger TP2 when price below target (better for short)', () => {
|
||||
const trade = createShortTrade()
|
||||
|
||||
expect(shouldTakeProfit2(137.00, trade)).toBe(true)
|
||||
expect(shouldTakeProfit2(135.00, trade)).toBe(true)
|
||||
})
|
||||
|
||||
it('should NOT trigger TP2 when price above target', () => {
|
||||
const trade = createShortTrade()
|
||||
|
||||
expect(shouldTakeProfit2(138.00, trade)).toBe(false)
|
||||
expect(shouldTakeProfit2(140.00, trade)).toBe(false)
|
||||
expect(shouldTakeProfit2(141.00, trade)).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Decision order priority', () => {
|
||||
it('emergency stop should trigger before regular SL', () => {
|
||||
const trade = createLongTrade({ entryPrice: 140 })
|
||||
const price = 136.00 // Below both emergency and SL
|
||||
|
||||
const emergencyTriggered = shouldEmergencyStop(price, trade)
|
||||
const slTriggered = shouldStopLoss(price, trade)
|
||||
|
||||
expect(emergencyTriggered).toBe(true)
|
||||
expect(slTriggered).toBe(true)
|
||||
// In Position Manager, emergency is checked first (higher priority)
|
||||
})
|
||||
|
||||
it('SL should NOT be checked if TP already hit the threshold', () => {
|
||||
const trade = createLongTrade({ entryPrice: 140 })
|
||||
const highPrice = 143.00 // Well above TP1 and TP2
|
||||
|
||||
const tp1Triggered = shouldTakeProfit1(highPrice, trade)
|
||||
const tp2Triggered = shouldTakeProfit2(highPrice, trade)
|
||||
const slTriggered = shouldStopLoss(highPrice, trade)
|
||||
|
||||
expect(tp1Triggered).toBe(true)
|
||||
expect(tp2Triggered).toBe(true)
|
||||
expect(slTriggered).toBe(false)
|
||||
// When price is high, TP triggers but not SL
|
||||
})
|
||||
})
|
||||
})
|
||||
241
tests/integration/position-manager/edge-cases.test.ts
Normal file
241
tests/integration/position-manager/edge-cases.test.ts
Normal file
@@ -0,0 +1,241 @@
|
||||
/**
|
||||
* Edge Cases Tests
|
||||
*
|
||||
* Tests for edge cases and common pitfalls in Position Manager.
|
||||
*
|
||||
* Pitfalls tested:
|
||||
* - #24: Position.size as tokens, not USD
|
||||
* - #54: MAE/MFE as percentages, not dollars
|
||||
* - Phantom trade detection (< 50% expected size)
|
||||
* - Profit percent calculation for LONG and SHORT
|
||||
*/
|
||||
|
||||
import {
|
||||
createLongTrade,
|
||||
createShortTrade,
|
||||
TEST_DEFAULTS,
|
||||
calculateExpectedProfitPercent
|
||||
} from '../../helpers/trade-factory'
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
describe('Position.size tokens vs USD (Pitfall #24)', () => {
|
||||
it('should convert token size to USD correctly', () => {
|
||||
// Drift SDK returns position.size in BASE ASSET TOKENS (e.g., 12.28 SOL)
|
||||
// NOT in USD ($1,950)
|
||||
|
||||
const positionSizeTokens = 12.28 // SOL tokens
|
||||
const currentPrice = 159.12 // Current SOL price
|
||||
|
||||
// CORRECT: Convert tokens to USD
|
||||
const positionSizeUSD = Math.abs(positionSizeTokens) * currentPrice
|
||||
expect(positionSizeUSD).toBeCloseTo(1953.59, 0)
|
||||
|
||||
// WRONG: Using tokens directly as USD (off by 159x!)
|
||||
expect(positionSizeTokens).not.toBe(positionSizeUSD)
|
||||
})
|
||||
|
||||
it('should detect TP1 using USD values, not token values', () => {
|
||||
// Bug: Comparing tokens (12.28) to USD ($1,950) caused false TP1 detection
|
||||
// 12.28 < 1950 * 0.95 was always true!
|
||||
|
||||
const trackedSizeUSD = 1950
|
||||
const positionSizeTokens = 12.28
|
||||
const currentPrice = 159.12
|
||||
|
||||
// WRONG: Direct comparison (would always think 95% was reduced)
|
||||
const wrongComparison = positionSizeTokens < trackedSizeUSD * 0.95
|
||||
expect(wrongComparison).toBe(true) // BUG: False positive!
|
||||
|
||||
// CORRECT: Convert to USD first
|
||||
const positionSizeUSD = Math.abs(positionSizeTokens) * currentPrice
|
||||
const correctComparison = positionSizeUSD < trackedSizeUSD * 0.95
|
||||
expect(correctComparison).toBe(false) // Position is actually ~100%, not reduced
|
||||
})
|
||||
|
||||
it('should calculate size reduction correctly using USD', () => {
|
||||
const originalSizeUSD = 8000
|
||||
const tp1SizePercent = 60
|
||||
|
||||
// After TP1, 60% closed, 40% remaining
|
||||
const expectedRemainingUSD = originalSizeUSD * (1 - tp1SizePercent / 100)
|
||||
expect(expectedRemainingUSD).toBe(3200)
|
||||
|
||||
// Token equivalent at $140/SOL
|
||||
const tokensRemaining = expectedRemainingUSD / 140
|
||||
expect(tokensRemaining).toBeCloseTo(22.86, 1)
|
||||
|
||||
// Verify conversion back to USD
|
||||
const convertedBackUSD = tokensRemaining * 140
|
||||
expect(convertedBackUSD).toBeCloseTo(expectedRemainingUSD, 0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Phantom trade detection (< 50% expected size)', () => {
|
||||
it('should detect phantom when actual size < 50% of expected', () => {
|
||||
const expectedSizeUSD = 8000
|
||||
const actualSizeUSD = 1370 // Only 17% filled
|
||||
|
||||
const sizeRatio = actualSizeUSD / expectedSizeUSD
|
||||
const isPhantom = sizeRatio < 0.5
|
||||
|
||||
expect(sizeRatio).toBeCloseTo(0.171, 2)
|
||||
expect(isPhantom).toBe(true)
|
||||
})
|
||||
|
||||
it('should NOT detect phantom when size >= 50%', () => {
|
||||
const expectedSizeUSD = 8000
|
||||
const actualSizeUSD = 4500 // 56% filled
|
||||
|
||||
const sizeRatio = actualSizeUSD / expectedSizeUSD
|
||||
const isPhantom = sizeRatio < 0.5
|
||||
|
||||
expect(sizeRatio).toBeCloseTo(0.5625, 2)
|
||||
expect(isPhantom).toBe(false)
|
||||
})
|
||||
|
||||
it('should handle exact 50% boundary', () => {
|
||||
const expectedSizeUSD = 8000
|
||||
const actualSizeUSD = 4000 // Exactly 50%
|
||||
|
||||
const sizeRatio = actualSizeUSD / expectedSizeUSD
|
||||
const isPhantom = sizeRatio < 0.5
|
||||
|
||||
expect(sizeRatio).toBe(0.5)
|
||||
expect(isPhantom).toBe(false) // 50% is acceptable
|
||||
})
|
||||
|
||||
it('should NOT flag runner after TP1 as phantom', () => {
|
||||
// Bug: After TP1, currentSize is 40% of original
|
||||
// This should NOT be flagged as phantom
|
||||
|
||||
const trade = createLongTrade({ tp1Hit: true })
|
||||
trade.currentSize = trade.positionSize * 0.4 // 40% remaining
|
||||
|
||||
// Phantom check should ONLY run on initial position, not after TP1
|
||||
const isAfterTP1 = trade.tp1Hit
|
||||
const sizeRatio = trade.currentSize / trade.positionSize
|
||||
|
||||
// Even though size is <50%, this is NOT a phantom - it's a runner
|
||||
const isPhantom = !isAfterTP1 && sizeRatio < 0.5
|
||||
|
||||
expect(isAfterTP1).toBe(true)
|
||||
expect(sizeRatio).toBe(0.4)
|
||||
expect(isPhantom).toBe(false) // Correctly NOT flagged as phantom
|
||||
})
|
||||
})
|
||||
|
||||
describe('MAE/MFE as percentages (Pitfall #54)', () => {
|
||||
it('should track MFE as percentage, not dollars', () => {
|
||||
const trade = createLongTrade({ entryPrice: 140 })
|
||||
const bestPrice = 141.20 // +0.86%
|
||||
|
||||
// CORRECT: Store as percentage
|
||||
const mfePercent = ((bestPrice - trade.entryPrice) / trade.entryPrice) * 100
|
||||
expect(mfePercent).toBeCloseTo(0.857, 1)
|
||||
|
||||
// Database expects small % values like 0.86, not $68
|
||||
expect(mfePercent).toBeLessThan(5) // Sanity check: percentage is small
|
||||
})
|
||||
|
||||
it('should track MAE as percentage, not dollars', () => {
|
||||
const trade = createLongTrade({ entryPrice: 140 })
|
||||
const worstPrice = 138.60 // -1%
|
||||
|
||||
// CORRECT: Store as percentage (negative for loss)
|
||||
const maePercent = ((worstPrice - trade.entryPrice) / trade.entryPrice) * 100
|
||||
expect(maePercent).toBeCloseTo(-1.0, 1)
|
||||
|
||||
// MAE should be negative for adverse movement
|
||||
expect(maePercent).toBeLessThan(0)
|
||||
})
|
||||
|
||||
it('should update MFE when profit increases', () => {
|
||||
const trade = createLongTrade({ entryPrice: 140 })
|
||||
trade.maxFavorableExcursion = 0
|
||||
|
||||
// Price moves to +0.5%, then +1%
|
||||
const prices = [140.70, 141.40]
|
||||
|
||||
for (const price of prices) {
|
||||
const profitPercent = ((price - trade.entryPrice) / trade.entryPrice) * 100
|
||||
if (profitPercent > trade.maxFavorableExcursion) {
|
||||
trade.maxFavorableExcursion = profitPercent
|
||||
trade.maxFavorablePrice = price
|
||||
}
|
||||
}
|
||||
|
||||
expect(trade.maxFavorableExcursion).toBeCloseTo(1.0, 1) // +1%
|
||||
expect(trade.maxFavorablePrice).toBe(141.40)
|
||||
})
|
||||
|
||||
it('should update MAE when loss increases', () => {
|
||||
const trade = createLongTrade({ entryPrice: 140 })
|
||||
trade.maxAdverseExcursion = 0
|
||||
|
||||
// Price moves to -0.3%, then -0.5%
|
||||
const prices = [139.58, 139.30]
|
||||
|
||||
for (const price of prices) {
|
||||
const profitPercent = ((price - trade.entryPrice) / trade.entryPrice) * 100
|
||||
if (profitPercent < trade.maxAdverseExcursion) {
|
||||
trade.maxAdverseExcursion = profitPercent
|
||||
trade.maxAdversePrice = price
|
||||
}
|
||||
}
|
||||
|
||||
expect(trade.maxAdverseExcursion).toBeCloseTo(-0.5, 1) // -0.5%
|
||||
expect(trade.maxAdversePrice).toBe(139.30)
|
||||
})
|
||||
|
||||
it('SHORT: should calculate MFE correctly (positive when price drops)', () => {
|
||||
const trade = createShortTrade({ entryPrice: 140 })
|
||||
const bestPrice = 138.60 // -1% = good for SHORT
|
||||
|
||||
// For SHORT, profit when price drops
|
||||
const mfePercent = ((trade.entryPrice - bestPrice) / trade.entryPrice) * 100
|
||||
expect(mfePercent).toBeCloseTo(1.0, 1) // +1% profit for short
|
||||
expect(mfePercent).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('SHORT: should calculate MAE correctly (negative when price rises)', () => {
|
||||
const trade = createShortTrade({ entryPrice: 140 })
|
||||
const worstPrice = 141.40 // +1% = bad for SHORT
|
||||
|
||||
// For SHORT, loss when price rises
|
||||
const maePercent = ((trade.entryPrice - worstPrice) / trade.entryPrice) * 100
|
||||
expect(maePercent).toBeCloseTo(-1.0, 1) // -1% loss for short
|
||||
expect(maePercent).toBeLessThan(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Profit percent calculation', () => {
|
||||
it('LONG: positive profit when price increases', () => {
|
||||
const profit = calculateExpectedProfitPercent(140, 141.20, 'long')
|
||||
expect(profit).toBeCloseTo(0.857, 1)
|
||||
expect(profit).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('LONG: negative profit when price decreases', () => {
|
||||
const profit = calculateExpectedProfitPercent(140, 138.80, 'long')
|
||||
expect(profit).toBeCloseTo(-0.857, 1)
|
||||
expect(profit).toBeLessThan(0)
|
||||
})
|
||||
|
||||
it('SHORT: positive profit when price decreases', () => {
|
||||
const profit = calculateExpectedProfitPercent(140, 138.80, 'short')
|
||||
expect(profit).toBeCloseTo(0.857, 1)
|
||||
expect(profit).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('SHORT: negative profit when price increases', () => {
|
||||
const profit = calculateExpectedProfitPercent(140, 141.20, 'short')
|
||||
expect(profit).toBeCloseTo(-0.857, 1)
|
||||
expect(profit).toBeLessThan(0)
|
||||
})
|
||||
|
||||
it('should return 0 when price equals entry', () => {
|
||||
expect(calculateExpectedProfitPercent(140, 140, 'long')).toBe(0)
|
||||
expect(calculateExpectedProfitPercent(140, 140, 'short')).toBe(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
197
tests/integration/position-manager/price-verification.test.ts
Normal file
197
tests/integration/position-manager/price-verification.test.ts
Normal file
@@ -0,0 +1,197 @@
|
||||
/**
|
||||
* Price Verification Tests
|
||||
*
|
||||
* Tests for price verification before setting TP flags.
|
||||
*
|
||||
* Key behaviors tested (Pitfall #43):
|
||||
* - Verify price reached TP1 before setting tp1Hit flag
|
||||
* - Require BOTH size reduced AND price at target
|
||||
* - Use tolerance for price target matching (0.2%)
|
||||
*/
|
||||
|
||||
import {
|
||||
createLongTrade,
|
||||
createShortTrade,
|
||||
TEST_DEFAULTS
|
||||
} from '../../helpers/trade-factory'
|
||||
|
||||
describe('Price Verification', () => {
|
||||
// Extract isPriceAtTarget logic from Position Manager
|
||||
function isPriceAtTarget(currentPrice: number, targetPrice: number, tolerance: number = 0.002): boolean {
|
||||
if (!targetPrice || targetPrice === 0) return false
|
||||
const diff = Math.abs(currentPrice - targetPrice) / targetPrice
|
||||
return diff <= tolerance
|
||||
}
|
||||
|
||||
function shouldTakeProfit1(price: number, trade: { direction: 'long' | 'short', tp1Price: number }): boolean {
|
||||
if (trade.direction === 'long') {
|
||||
return price >= trade.tp1Price
|
||||
} else {
|
||||
return price <= trade.tp1Price
|
||||
}
|
||||
}
|
||||
|
||||
describe('TP1 verification before setting flag (Pitfall #43)', () => {
|
||||
it('LONG: should require BOTH size reduction AND price at TP1', () => {
|
||||
const trade = createLongTrade({ entryPrice: 140, tp1Price: 141.20 })
|
||||
|
||||
// Scenario: Size reduced but price NOT at TP1
|
||||
const sizeReduced = true
|
||||
const currentPrice = 140.50 // Below TP1
|
||||
const priceAtTP1 = isPriceAtTarget(currentPrice, trade.tp1Price)
|
||||
|
||||
// Should NOT set tp1Hit because price hasn't reached target
|
||||
const shouldSetTP1 = sizeReduced && priceAtTP1
|
||||
expect(shouldSetTP1).toBe(false)
|
||||
|
||||
// This was likely a MANUAL CLOSE, not TP1
|
||||
})
|
||||
|
||||
it('LONG: should allow TP1 when both conditions met', () => {
|
||||
const trade = createLongTrade({ entryPrice: 140, tp1Price: 141.20 })
|
||||
|
||||
// Scenario: Size reduced AND price at TP1
|
||||
const sizeReduced = true
|
||||
const currentPrice = 141.20
|
||||
const priceAtTP1 = isPriceAtTarget(currentPrice, trade.tp1Price)
|
||||
|
||||
const shouldSetTP1 = sizeReduced && priceAtTP1
|
||||
expect(shouldSetTP1).toBe(true)
|
||||
})
|
||||
|
||||
it('SHORT: should require BOTH size reduction AND price at TP1', () => {
|
||||
const trade = createShortTrade({ entryPrice: 140, tp1Price: 138.80 })
|
||||
|
||||
// Scenario: Size reduced but price NOT at TP1
|
||||
const sizeReduced = true
|
||||
const currentPrice = 139.50 // Above TP1 (short profits when price drops)
|
||||
const priceAtTP1 = isPriceAtTarget(currentPrice, trade.tp1Price)
|
||||
|
||||
const shouldSetTP1 = sizeReduced && priceAtTP1
|
||||
expect(shouldSetTP1).toBe(false)
|
||||
})
|
||||
|
||||
it('SHORT: should allow TP1 when both conditions met', () => {
|
||||
const trade = createShortTrade({ entryPrice: 140, tp1Price: 138.80 })
|
||||
|
||||
const sizeReduced = true
|
||||
const currentPrice = 138.80
|
||||
const priceAtTP1 = isPriceAtTarget(currentPrice, trade.tp1Price)
|
||||
|
||||
const shouldSetTP1 = sizeReduced && priceAtTP1
|
||||
expect(shouldSetTP1).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isPriceAtTarget tolerance (0.2%)', () => {
|
||||
it('should return true when price exactly at target', () => {
|
||||
const result = isPriceAtTarget(141.20, 141.20)
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
|
||||
it('should return true when price within 0.2% of target', () => {
|
||||
// 0.2% of 141.20 = 0.2824
|
||||
// So prices from 140.92 to 141.48 should be "at target"
|
||||
|
||||
expect(isPriceAtTarget(141.10, 141.20)).toBe(true) // -0.07%
|
||||
expect(isPriceAtTarget(141.30, 141.20)).toBe(true) // +0.07%
|
||||
expect(isPriceAtTarget(141.00, 141.20)).toBe(true) // -0.14%
|
||||
expect(isPriceAtTarget(141.40, 141.20)).toBe(true) // +0.14%
|
||||
})
|
||||
|
||||
it('should return false when price outside 0.2% tolerance', () => {
|
||||
// More than 0.2% away should NOT be "at target"
|
||||
expect(isPriceAtTarget(140.70, 141.20)).toBe(false) // -0.35%
|
||||
expect(isPriceAtTarget(141.60, 141.20)).toBe(false) // +0.28%
|
||||
expect(isPriceAtTarget(140.00, 141.20)).toBe(false) // -0.85%
|
||||
})
|
||||
|
||||
it('should handle edge case of 0 target price', () => {
|
||||
const result = isPriceAtTarget(141.20, 0)
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
|
||||
it('should handle custom tolerance', () => {
|
||||
// Use stricter 0.1% tolerance
|
||||
expect(isPriceAtTarget(141.10, 141.20, 0.001)).toBe(true) // 0.07% < 0.1%
|
||||
expect(isPriceAtTarget(141.00, 141.20, 0.001)).toBe(false) // 0.14% > 0.1%
|
||||
|
||||
// Use looser 0.5% tolerance
|
||||
expect(isPriceAtTarget(140.50, 141.20, 0.005)).toBe(true) // 0.5% = 0.5%
|
||||
expect(isPriceAtTarget(140.00, 141.20, 0.005)).toBe(false) // 0.85% > 0.5%
|
||||
})
|
||||
})
|
||||
|
||||
describe('shouldTakeProfit1 with price verification', () => {
|
||||
it('LONG: combined size + price verification', () => {
|
||||
const trade = createLongTrade({
|
||||
entryPrice: 140,
|
||||
tp1Price: 141.20,
|
||||
positionSize: 8000
|
||||
})
|
||||
|
||||
// Simulate Position Manager logic
|
||||
const originalSize = trade.positionSize
|
||||
const currentSize = 3200 // After 60% close
|
||||
const sizeMismatch = currentSize < originalSize * 0.9
|
||||
|
||||
// Case 1: Price at TP1
|
||||
const priceAtTP1 = 141.20
|
||||
const priceReachedTP1 = shouldTakeProfit1(priceAtTP1, trade)
|
||||
expect(sizeMismatch && priceReachedTP1).toBe(true) // Valid TP1
|
||||
|
||||
// Case 2: Price NOT at TP1
|
||||
const priceNotAtTP1 = 140.50
|
||||
const priceReachedTP1_2 = shouldTakeProfit1(priceNotAtTP1, trade)
|
||||
expect(sizeMismatch && priceReachedTP1_2).toBe(false) // Invalid - manual close
|
||||
})
|
||||
|
||||
it('SHORT: combined size + price verification', () => {
|
||||
const trade = createShortTrade({
|
||||
entryPrice: 140,
|
||||
tp1Price: 138.80,
|
||||
positionSize: 8000
|
||||
})
|
||||
|
||||
const originalSize = trade.positionSize
|
||||
const currentSize = 3200
|
||||
const sizeMismatch = currentSize < originalSize * 0.9
|
||||
|
||||
// Case 1: Price at TP1 (below entry for short)
|
||||
const priceAtTP1 = 138.80
|
||||
const priceReachedTP1 = shouldTakeProfit1(priceAtTP1, trade)
|
||||
expect(sizeMismatch && priceReachedTP1).toBe(true)
|
||||
|
||||
// Case 2: Price NOT at TP1 (still above target)
|
||||
const priceNotAtTP1 = 139.50
|
||||
const priceReachedTP1_2 = shouldTakeProfit1(priceNotAtTP1, trade)
|
||||
expect(sizeMismatch && priceReachedTP1_2).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('TP2 price verification', () => {
|
||||
function shouldTakeProfit2(price: number, trade: { direction: 'long' | 'short', tp2Price: number }): boolean {
|
||||
if (trade.direction === 'long') {
|
||||
return price >= trade.tp2Price
|
||||
} else {
|
||||
return price <= trade.tp2Price
|
||||
}
|
||||
}
|
||||
|
||||
it('LONG: should detect TP2 at correct price', () => {
|
||||
const trade = createLongTrade({ tp2Price: 142.41 })
|
||||
|
||||
expect(shouldTakeProfit2(142.00, trade)).toBe(false) // Below
|
||||
expect(shouldTakeProfit2(142.41, trade)).toBe(true) // At target
|
||||
expect(shouldTakeProfit2(143.00, trade)).toBe(true) // Above
|
||||
})
|
||||
|
||||
it('SHORT: should detect TP2 at correct price', () => {
|
||||
const trade = createShortTrade({ tp2Price: 137.59 })
|
||||
|
||||
expect(shouldTakeProfit2(138.00, trade)).toBe(false) // Above (bad for short)
|
||||
expect(shouldTakeProfit2(137.59, trade)).toBe(true) // At target
|
||||
expect(shouldTakeProfit2(137.00, trade)).toBe(true) // Below (better for short)
|
||||
})
|
||||
})
|
||||
})
|
||||
177
tests/integration/position-manager/tp1-detection.test.ts
Normal file
177
tests/integration/position-manager/tp1-detection.test.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
/**
|
||||
* TP1 Detection Tests
|
||||
*
|
||||
* Tests for Take Profit 1 detection in Position Manager.
|
||||
* TP1 is calculated as ATR × 2.0 (typically ~0.86% at ATR 0.43)
|
||||
*
|
||||
* Key behaviors tested:
|
||||
* - LONG: TP1 triggers when price >= tp1Price
|
||||
* - SHORT: TP1 triggers when price <= tp1Price
|
||||
* - Must NOT trigger below threshold
|
||||
* - Must NOT trigger when price moves against position
|
||||
*/
|
||||
|
||||
import {
|
||||
createLongTrade,
|
||||
createShortTrade,
|
||||
TEST_DEFAULTS,
|
||||
calculateExpectedProfitPercent
|
||||
} from '../../helpers/trade-factory'
|
||||
|
||||
describe('TP1 Detection', () => {
|
||||
// Test the shouldTakeProfit1 logic extracted from Position Manager
|
||||
// We test the pure calculation logic rather than the full Position Manager
|
||||
|
||||
function shouldTakeProfit1(price: number, trade: { direction: 'long' | 'short', tp1Price: number }): boolean {
|
||||
if (trade.direction === 'long') {
|
||||
return price >= trade.tp1Price
|
||||
} else {
|
||||
return price <= trade.tp1Price
|
||||
}
|
||||
}
|
||||
|
||||
describe('LONG positions', () => {
|
||||
it('should detect TP1 when price reaches +0.86% (ATR 0.43 × 2.0)', () => {
|
||||
// Test data: entry $140, TP1 at $141.20 (+0.86%)
|
||||
const trade = createLongTrade()
|
||||
|
||||
expect(trade.tp1Price).toBe(TEST_DEFAULTS.long.tp1)
|
||||
|
||||
// Price at TP1 should trigger
|
||||
const result = shouldTakeProfit1(141.20, trade)
|
||||
expect(result).toBe(true)
|
||||
|
||||
// Verify profit calculation
|
||||
const profitPercent = calculateExpectedProfitPercent(140, 141.20, 'long')
|
||||
expect(profitPercent).toBeCloseTo(0.857, 1) // ~0.86%
|
||||
})
|
||||
|
||||
it('should detect TP1 when price exceeds tp1Price', () => {
|
||||
const trade = createLongTrade()
|
||||
|
||||
// Price above TP1 should also trigger
|
||||
const result = shouldTakeProfit1(142.00, trade)
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
|
||||
it('should NOT detect TP1 when price is below threshold (+0.5%)', () => {
|
||||
const trade = createLongTrade()
|
||||
|
||||
// Price at +0.5% ($140.70) should NOT trigger TP1
|
||||
const result = shouldTakeProfit1(140.70, trade)
|
||||
expect(result).toBe(false)
|
||||
|
||||
// Verify profit percent is below TP1 threshold
|
||||
const profitPercent = calculateExpectedProfitPercent(140, 140.70, 'long')
|
||||
expect(profitPercent).toBeCloseTo(0.5, 2)
|
||||
expect(profitPercent).toBeLessThan(0.86)
|
||||
})
|
||||
|
||||
it('should NOT detect TP1 when price moves against position (negative)', () => {
|
||||
const trade = createLongTrade()
|
||||
|
||||
// Price drop for LONG should NOT trigger TP1
|
||||
const result = shouldTakeProfit1(139.00, trade)
|
||||
expect(result).toBe(false)
|
||||
|
||||
// Verify this is a loss
|
||||
const profitPercent = calculateExpectedProfitPercent(140, 139.00, 'long')
|
||||
expect(profitPercent).toBeLessThan(0)
|
||||
})
|
||||
|
||||
it('should NOT detect TP1 at entry price', () => {
|
||||
const trade = createLongTrade()
|
||||
|
||||
const result = shouldTakeProfit1(140.00, trade)
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('SHORT positions', () => {
|
||||
it('should detect TP1 when price reaches -0.86% (ATR 0.43 × 2.0)', () => {
|
||||
// Test data: entry $140, TP1 at $138.80 (-0.86%)
|
||||
const trade = createShortTrade()
|
||||
|
||||
expect(trade.tp1Price).toBe(TEST_DEFAULTS.short.tp1)
|
||||
|
||||
// Price at TP1 should trigger
|
||||
const result = shouldTakeProfit1(138.80, trade)
|
||||
expect(result).toBe(true)
|
||||
|
||||
// Verify profit calculation (SHORT profits when price drops)
|
||||
const profitPercent = calculateExpectedProfitPercent(140, 138.80, 'short')
|
||||
expect(profitPercent).toBeCloseTo(0.857, 1) // ~0.86%
|
||||
})
|
||||
|
||||
it('should detect TP1 when price is below tp1Price', () => {
|
||||
const trade = createShortTrade()
|
||||
|
||||
// Price below TP1 should also trigger (better for short)
|
||||
const result = shouldTakeProfit1(138.00, trade)
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
|
||||
it('should NOT detect TP1 when price is above threshold (-0.5%)', () => {
|
||||
const trade = createShortTrade()
|
||||
|
||||
// Price at -0.5% ($139.30) should NOT trigger TP1
|
||||
const result = shouldTakeProfit1(139.30, trade)
|
||||
expect(result).toBe(false)
|
||||
|
||||
// Verify profit percent is below TP1 threshold
|
||||
const profitPercent = calculateExpectedProfitPercent(140, 139.30, 'short')
|
||||
expect(profitPercent).toBeCloseTo(0.5, 2)
|
||||
expect(profitPercent).toBeLessThan(0.86)
|
||||
})
|
||||
|
||||
it('should NOT detect TP1 when price moves against position (rises)', () => {
|
||||
const trade = createShortTrade()
|
||||
|
||||
// Price rise for SHORT should NOT trigger TP1
|
||||
const result = shouldTakeProfit1(141.00, trade)
|
||||
expect(result).toBe(false)
|
||||
|
||||
// Verify this is a loss for SHORT
|
||||
const profitPercent = calculateExpectedProfitPercent(140, 141.00, 'short')
|
||||
expect(profitPercent).toBeLessThan(0)
|
||||
})
|
||||
|
||||
it('should NOT detect TP1 at entry price', () => {
|
||||
const trade = createShortTrade()
|
||||
|
||||
const result = shouldTakeProfit1(140.00, trade)
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Edge cases', () => {
|
||||
it('should handle exact TP1 price (boundary condition)', () => {
|
||||
const longTrade = createLongTrade()
|
||||
const shortTrade = createShortTrade()
|
||||
|
||||
// Exact TP1 price should trigger
|
||||
expect(shouldTakeProfit1(longTrade.tp1Price, longTrade)).toBe(true)
|
||||
expect(shouldTakeProfit1(shortTrade.tp1Price, shortTrade)).toBe(true)
|
||||
})
|
||||
|
||||
it('should handle custom TP1 prices', () => {
|
||||
const trade = createLongTrade({ tp1Price: 145.00 })
|
||||
|
||||
expect(trade.tp1Price).toBe(145.00)
|
||||
expect(shouldTakeProfit1(144.99, trade)).toBe(false)
|
||||
expect(shouldTakeProfit1(145.00, trade)).toBe(true)
|
||||
expect(shouldTakeProfit1(145.01, trade)).toBe(true)
|
||||
})
|
||||
|
||||
it('should work with different entry prices', () => {
|
||||
// ETH-PERP style entry at $3500
|
||||
const trade = createLongTrade({
|
||||
entryPrice: 3500,
|
||||
tp1Price: 3530, // +0.86%
|
||||
})
|
||||
|
||||
expect(shouldTakeProfit1(3529, trade)).toBe(false)
|
||||
expect(shouldTakeProfit1(3530, trade)).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
271
tests/integration/position-manager/trailing-stop.test.ts
Normal file
271
tests/integration/position-manager/trailing-stop.test.ts
Normal file
@@ -0,0 +1,271 @@
|
||||
/**
|
||||
* Trailing Stop Tests
|
||||
*
|
||||
* Tests for trailing stop functionality after TP2 trigger.
|
||||
*
|
||||
* Key behaviors tested:
|
||||
* - Trailing stop activates after TP2 trigger
|
||||
* - Calculate ATR-based trailing distance (1.5x ATR)
|
||||
* - Update peak price tracking as price moves favorably
|
||||
* - Trigger exit when price falls below trailing stop
|
||||
*/
|
||||
|
||||
import {
|
||||
createLongTrade,
|
||||
createShortTrade,
|
||||
createTradeAfterTP2,
|
||||
TEST_DEFAULTS
|
||||
} from '../../helpers/trade-factory'
|
||||
|
||||
describe('Trailing Stop', () => {
|
||||
// Extract trailing stop calculation logic from Position Manager
|
||||
function calculateTrailingStopPrice(
|
||||
peakPrice: number,
|
||||
trailingDistancePercent: number,
|
||||
direction: 'long' | 'short'
|
||||
): number {
|
||||
if (direction === 'long') {
|
||||
return peakPrice * (1 - trailingDistancePercent / 100)
|
||||
} else {
|
||||
return peakPrice * (1 + trailingDistancePercent / 100)
|
||||
}
|
||||
}
|
||||
|
||||
function calculateAtrBasedTrailingDistance(
|
||||
atr: number,
|
||||
currentPrice: number,
|
||||
multiplier: number,
|
||||
minPercent: number,
|
||||
maxPercent: number
|
||||
): number {
|
||||
const atrPercent = (atr / currentPrice) * 100
|
||||
const rawDistance = atrPercent * multiplier
|
||||
return Math.max(minPercent, Math.min(maxPercent, rawDistance))
|
||||
}
|
||||
|
||||
function shouldStopLoss(price: number, trade: { direction: 'long' | 'short', stopLossPrice: number }): boolean {
|
||||
if (trade.direction === 'long') {
|
||||
return price <= trade.stopLossPrice
|
||||
} else {
|
||||
return price >= trade.stopLossPrice
|
||||
}
|
||||
}
|
||||
|
||||
describe('Trailing stop activation', () => {
|
||||
it('should activate trailing stop after TP2 trigger', () => {
|
||||
const trade = createTradeAfterTP2('long')
|
||||
|
||||
expect(trade.tp2Hit).toBe(true)
|
||||
expect(trade.trailingStopActive).toBe(true)
|
||||
})
|
||||
|
||||
it('should NOT have trailing stop before TP2', () => {
|
||||
const trade = createLongTrade()
|
||||
|
||||
expect(trade.tp2Hit).toBe(false)
|
||||
expect(trade.trailingStopActive).toBe(false)
|
||||
})
|
||||
|
||||
it('should NOT have trailing stop after TP1 only', () => {
|
||||
const trade = createLongTrade({ tp1Hit: true })
|
||||
|
||||
expect(trade.tp1Hit).toBe(true)
|
||||
expect(trade.tp2Hit).toBe(false)
|
||||
expect(trade.trailingStopActive).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('ATR-based trailing distance calculation', () => {
|
||||
it('should calculate trailing distance as 1.5x ATR', () => {
|
||||
// ATR 0.43 at price $140
|
||||
const atr = TEST_DEFAULTS.atr
|
||||
const currentPrice = TEST_DEFAULTS.entry
|
||||
const multiplier = 1.5
|
||||
const minPercent = 0.25
|
||||
const maxPercent = 0.9
|
||||
|
||||
const distance = calculateAtrBasedTrailingDistance(
|
||||
atr, currentPrice, multiplier, minPercent, maxPercent
|
||||
)
|
||||
|
||||
// 0.43 / 140 * 100 = 0.307% * 1.5 = 0.46%
|
||||
expect(distance).toBeCloseTo(0.46, 1)
|
||||
})
|
||||
|
||||
it('should clamp trailing distance to minimum', () => {
|
||||
// Very low ATR should clamp to min
|
||||
const atr = 0.1
|
||||
const currentPrice = 140
|
||||
const multiplier = 1.5
|
||||
const minPercent = 0.25
|
||||
const maxPercent = 0.9
|
||||
|
||||
const distance = calculateAtrBasedTrailingDistance(
|
||||
atr, currentPrice, multiplier, minPercent, maxPercent
|
||||
)
|
||||
|
||||
// 0.1 / 140 * 100 = 0.071% * 1.5 = 0.107% < min 0.25%
|
||||
expect(distance).toBe(minPercent)
|
||||
})
|
||||
|
||||
it('should clamp trailing distance to maximum', () => {
|
||||
// Very high ATR should clamp to max
|
||||
const atr = 2.0
|
||||
const currentPrice = 140
|
||||
const multiplier = 1.5
|
||||
const minPercent = 0.25
|
||||
const maxPercent = 0.9
|
||||
|
||||
const distance = calculateAtrBasedTrailingDistance(
|
||||
atr, currentPrice, multiplier, minPercent, maxPercent
|
||||
)
|
||||
|
||||
// 2.0 / 140 * 100 = 1.43% * 1.5 = 2.14% > max 0.9%
|
||||
expect(distance).toBe(maxPercent)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Peak price tracking', () => {
|
||||
it('LONG: should update peak price when price increases', () => {
|
||||
const trade = createTradeAfterTP2('long', { peakPrice: 142.41 })
|
||||
|
||||
const newHighPrice = 143.00
|
||||
|
||||
// Simulate peak price update logic
|
||||
if (newHighPrice > trade.peakPrice) {
|
||||
trade.peakPrice = newHighPrice
|
||||
}
|
||||
|
||||
expect(trade.peakPrice).toBe(143.00)
|
||||
})
|
||||
|
||||
it('LONG: should NOT update peak price when price decreases', () => {
|
||||
const trade = createTradeAfterTP2('long', { peakPrice: 142.41 })
|
||||
|
||||
const lowerPrice = 142.00
|
||||
|
||||
// Simulate peak price update logic
|
||||
if (lowerPrice > trade.peakPrice) {
|
||||
trade.peakPrice = lowerPrice
|
||||
}
|
||||
|
||||
expect(trade.peakPrice).toBe(142.41) // Unchanged
|
||||
})
|
||||
|
||||
it('SHORT: should update peak price when price decreases', () => {
|
||||
const trade = createTradeAfterTP2('short', { peakPrice: 137.59 })
|
||||
|
||||
const newLowPrice = 137.00
|
||||
|
||||
// Simulate peak price update logic (for short, lower is better)
|
||||
if (newLowPrice < trade.peakPrice) {
|
||||
trade.peakPrice = newLowPrice
|
||||
}
|
||||
|
||||
expect(trade.peakPrice).toBe(137.00)
|
||||
})
|
||||
|
||||
it('SHORT: should NOT update peak price when price increases', () => {
|
||||
const trade = createTradeAfterTP2('short', { peakPrice: 137.59 })
|
||||
|
||||
const higherPrice = 138.00
|
||||
|
||||
// Simulate peak price update logic
|
||||
if (higherPrice < trade.peakPrice) {
|
||||
trade.peakPrice = higherPrice
|
||||
}
|
||||
|
||||
expect(trade.peakPrice).toBe(137.59) // Unchanged
|
||||
})
|
||||
})
|
||||
|
||||
describe('Trailing stop trigger', () => {
|
||||
it('LONG: should trigger when price falls below trailing SL', () => {
|
||||
const peakPrice = 143.00
|
||||
const trailingPercent = 0.46 // ~0.46% trail
|
||||
const trailingSL = calculateTrailingStopPrice(peakPrice, trailingPercent, 'long')
|
||||
|
||||
// Trail SL = 143 * (1 - 0.0046) = 142.34
|
||||
expect(trailingSL).toBeCloseTo(142.34, 1)
|
||||
|
||||
// Price above trail should not trigger
|
||||
expect(shouldStopLoss(142.50, { direction: 'long', stopLossPrice: trailingSL })).toBe(false)
|
||||
|
||||
// Price at trail should trigger
|
||||
expect(shouldStopLoss(trailingSL, { direction: 'long', stopLossPrice: trailingSL })).toBe(true)
|
||||
|
||||
// Price below trail should trigger
|
||||
expect(shouldStopLoss(142.00, { direction: 'long', stopLossPrice: trailingSL })).toBe(true)
|
||||
})
|
||||
|
||||
it('SHORT: should trigger when price rises above trailing SL', () => {
|
||||
const peakPrice = 137.00
|
||||
const trailingPercent = 0.46
|
||||
const trailingSL = calculateTrailingStopPrice(peakPrice, trailingPercent, 'short')
|
||||
|
||||
// Trail SL = 137 * (1 + 0.0046) = 137.63
|
||||
expect(trailingSL).toBeCloseTo(137.63, 1)
|
||||
|
||||
// Price below trail should not trigger
|
||||
expect(shouldStopLoss(137.30, { direction: 'short', stopLossPrice: trailingSL })).toBe(false)
|
||||
|
||||
// Price at trail should trigger
|
||||
expect(shouldStopLoss(trailingSL, { direction: 'short', stopLossPrice: trailingSL })).toBe(true)
|
||||
|
||||
// Price above trail should trigger
|
||||
expect(shouldStopLoss(138.00, { direction: 'short', stopLossPrice: trailingSL })).toBe(true)
|
||||
})
|
||||
|
||||
it('LONG: trailing SL should move up but never down', () => {
|
||||
let currentTrailingSL = 142.00
|
||||
const trailingPercent = 0.46
|
||||
|
||||
// Price rises to 143, new trail = 142.34
|
||||
const newTrailingSL1 = calculateTrailingStopPrice(143.00, trailingPercent, 'long')
|
||||
if (newTrailingSL1 > currentTrailingSL) {
|
||||
currentTrailingSL = newTrailingSL1
|
||||
}
|
||||
expect(currentTrailingSL).toBeCloseTo(142.34, 1)
|
||||
|
||||
// Price drops to 142.50, trail should NOT move down
|
||||
const newTrailingSL2 = calculateTrailingStopPrice(142.50, trailingPercent, 'long')
|
||||
if (newTrailingSL2 > currentTrailingSL) {
|
||||
currentTrailingSL = newTrailingSL2
|
||||
}
|
||||
expect(currentTrailingSL).toBeCloseTo(142.34, 1) // Unchanged
|
||||
|
||||
// Price rises to 144, trail should move up
|
||||
const newTrailingSL3 = calculateTrailingStopPrice(144.00, trailingPercent, 'long')
|
||||
if (newTrailingSL3 > currentTrailingSL) {
|
||||
currentTrailingSL = newTrailingSL3
|
||||
}
|
||||
expect(currentTrailingSL).toBeCloseTo(143.34, 1) // Moved up
|
||||
})
|
||||
|
||||
it('SHORT: trailing SL should move down but never up', () => {
|
||||
let currentTrailingSL = 138.00
|
||||
const trailingPercent = 0.46
|
||||
|
||||
// Price drops to 137, new trail = 137.63
|
||||
const newTrailingSL1 = calculateTrailingStopPrice(137.00, trailingPercent, 'short')
|
||||
if (newTrailingSL1 < currentTrailingSL) {
|
||||
currentTrailingSL = newTrailingSL1
|
||||
}
|
||||
expect(currentTrailingSL).toBeCloseTo(137.63, 1)
|
||||
|
||||
// Price rises to 137.50, trail should NOT move up
|
||||
const newTrailingSL2 = calculateTrailingStopPrice(137.50, trailingPercent, 'short')
|
||||
if (newTrailingSL2 < currentTrailingSL) {
|
||||
currentTrailingSL = newTrailingSL2
|
||||
}
|
||||
expect(currentTrailingSL).toBeCloseTo(137.63, 1) // Unchanged
|
||||
|
||||
// Price drops to 136, trail should move down
|
||||
const newTrailingSL3 = calculateTrailingStopPrice(136.00, trailingPercent, 'short')
|
||||
if (newTrailingSL3 < currentTrailingSL) {
|
||||
currentTrailingSL = newTrailingSL3
|
||||
}
|
||||
expect(currentTrailingSL).toBeCloseTo(136.63, 1) // Moved down
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user