Skip to content

Instantly share code, notes, and snippets.

@DerekZiemba
Last active March 15, 2024 11:46
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save DerekZiemba/41c7049b721fe3c12e8d5144d346e343 to your computer and use it in GitHub Desktop.
Save DerekZiemba/41c7049b721fe3c12e8d5144d346e343 to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name tradeogre-tampermonkey-script
// @author Derek Ziemba
// @version 2024.03.18
// @match https://tradeogre.com/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=tradeogre.com
// @updateUrl https://gist.github.com/DerekZiemba/41c7049b721fe3c12e8d5144d346e343.js
// @downloadUrl https://gist.github.com/DerekZiemba/41c7049b721fe3c12e8d5144d346e343.js
//// @require file:///C:/Dropbox/Scripts/.js/ZZ5/websites/tradeogre/tradeogre.js
// @run-at document-start
// @grant GM_addStyle
// @grant GM_addElement
// @grant GM_getValue
// @grant GM_setValue
// @grant unsafeWindow
// ==/UserScript==
/**
* @typedef {HTMLElementTagNameMap & SVGElementTagNameMap} ElementTagMap
*/
/**
* @typedef {`${keyof ElementTagMap}${' ' | '#' | '.' | '['}${string}` | `${string} ${keyof ElementTagMap}${' ' | '#' | '.' | '['}${string}` | keyof ElementTagMap} CssSelector
*/
/**
* @interface ParentNode
*/
/**
* @method ParentNode.querySelector
* @param {CssSelector} selectors
* @returns {ElementTagMap[keyof ElementTagMap]}
*/
/**
* @method ParentNode.querySelectorAll
* @param {CssSelector} selectors
* @returns {NodeListOf<ElementTagMap[keyof ElementTagMap]>}
*/
(()=>{
/**@template T */
extendType(/**@type {Array<T>}*/(Array.prototype), {
/**@typedef {(this: Array<T>, ...items: T[])=>boolean} pushUnique*/
/**@type {pushUnique} */
pushUnique(...items) {
var result = false;
for (var len = items.length, i = 0; i < len; i++) {
if (!this.includes(items[i])) {
this.push(items[i]);
result = true;
}
}
return result;
},
/**@typedef {(this: Array<T>, item: T)=>boolean} removeItem */
/**@type {removeItem} */
removeItem(item) {
var idx = this.indexOf(item);
if (idx >= 0) {
this.splice(idx, 1);
while ((idx = this.indexOf(item))>=0) { this.splice(idx, 1); }
return true;
}
return false;
},
/**@typedef {(this: Array<T>)=>Array<T>} distinct */
/**@type {distinct} */
distinct() {
const result = [];
for (let len = this.length, i = 0; i < len; ++i) {
const val = this[i];
if (!result.includes(val)) { result.push(val); }
}
return result;
},
});
/**@template {Element} T */
extendType(/**@type {NodeListOf<T>}*/(NodeList.prototype), {
toArray() { return Array.from(this); },
filter: Array.prototype.filter,
map: Array.prototype.map,
at: Array.prototype.at,
});
/**@template {Element} T */
extendType(/**@type {HTMLCollectionOf<T>}*/(HTMLCollection.prototype), {
toArray() { return Array.from(this); },
forEach: Array.prototype.forEach,
at: Array.prototype.at,
get values() { return this[Symbol.iterator]; }
});
/**@type {<T extends object, S extends object>(target: T, source: S)=>asserts target is T&S} */
function extendType(target, source) {
var entries = Array.isArray(source) ? source : Object.entries(Object.getOwnPropertyDescriptors(source));
for(var [name, desc] of entries) {
if (!(name in target)) {
desc.configurable = true;
desc.enumerable = false;
if (desc.writable === true && typeof desc.value === 'function') {
desc.writable = false;
}
Object.defineProperty(target, name, desc);
}
}
};
})();
var { html, css } = (()=>{ return { html, css };
function html(strings, ...values) {
const html = zipStringsAndValues(strings, values).join('');
const template = document.createElement('template');
template.innerHTML = html;
const fragment = template.content;
return /**@type {HTMLElement&DocumentFragment}*/(fragment.childElementCount === 1 ? fragment.firstElementChild : fragment);
}
function css(strings, ...values) {
const html = zipStringsAndValues(strings, values).join('');
const styleElement = document.createElement('style');
styleElement.innerHTML = html;
return styleElement;
}
function zipStringsAndValues(strings, values) {
const destination = [];
let lenS=strings.length, iS=0;
for(let lenV = values.length, iV=0; iS<lenS && iV<lenV; iV++, iS++) {
destination.push(strings[iS]);
Array.isArray(values[iV]) ? destination.push(...values[iV]) : destination.push(values[iV]);
}
for(; iS<lenS; iS++) { destination.push(strings[iS]); }
return destination;
}
})();
var { deferredPromise } = (()=>{
/** @template T @typedef {Promise<T>&{ resolve(x: T): void; reject(x: any): void; }} DeferredPromise */
var savedArgs = null;
var steal = (...args)=>{ savedArgs = args; }
/**@type {<T>(x?: Pick<Promise<T>,'then'>)=>DeferredPromise<T>} */
function deferredPromise(x) {
const prom = /**@type {DeferredPromise<any>} */(new Promise(steal));
[prom.resolve, prom.reject] = savedArgs; savedArgs = null;
if (x && typeof x.then === 'function') {
x.then(prom.resolve, prom.reject);
}
return prom;
}
return { deferredPromise };
})();
var { documentInteractive, documentLoaded, documentComplete } = (()=>{
var documentComplete = deferredPromise();
var documentLoaded = deferredPromise(documentComplete);
var documentInteractive = deferredPromise(documentLoaded);
var checkReadyStateChange = (ev)=> {
var readyState = document.readyState;
if (readyState === 'complete') {
documentComplete.resolve(document);
} else if (readyState === 'interactive') {
if (ev && ev.type === 'DOMContentLoaded') {
documentLoaded.resolve(document);
} else {
documentInteractive.resolve(document);
}
}
};
document.addEventListener('DOMContentLoaded', checkReadyStateChange);
document.addEventListener('readystatechange', checkReadyStateChange);
documentComplete.then((doc)=>{
doc.removeEventListener('DOMContentLoaded', checkReadyStateChange);
doc.removeEventListener('readystatechange', checkReadyStateChange);
});
checkReadyStateChange();
return { documentInteractive, documentLoaded, documentComplete };
})();
const storage = (()=>{
/**@type {<K extends string, T>(key: K, defaultV: T)=>((newValue?: T)=>Promise<T>)} */
function createAccessor(key, defaultValue) {
return async function accessor(value) {
if (value!==undefined) {
return GM.setValue(key, value);
} else {
return (await GM.getValue(key)) ?? defaultValue;
}
};
}
return {
shortcuts: createAccessor('navbar-crypto-shortcuts', { valid: /**@type {string[]} */([]), invalid: /**@type {string[]} */([]) })
};
})();
GM.addStyle(css`
#main > #main2 {
padding: 0;
> #chartc {
height: 55vh !important;
#marketinfo, #chart, & > * {
height: 100%;
}
#marketinfo {
min-width: unset;
}
}
> div {
> div[class*=container] {
> .bordered {
.ps:is(.ordersbody,.historybody)[style*="height:"] {
height: 29vh !important;
}
}
}
}
}
#main > #main2 > .exchange {
padding: 0 4px;
margin-top: -8px;
max-width: unset;
height: 100vh;
padding-bottom: 35px;
overflow: hidden;
> .orderbook {
.orderbookorder {
.orderbookitem {
width: auto;
}
}
}
> .userorders {
.userorderrow {
border-bottom: 1px ridge #7f7f7f7f;
padding: 2px 10px;
}
}
> .chart {
#klinechartcontainer {
padding-left: 10px;
> div {
> div:first-child {
> div:first-child {
}
}
}
}
.chartbuttonscontainer {
gap: 3px;
> .chartbutton {
margin: 0px 1px;
padding: 1px 6px;
}
}
}
> .marketinfo {
position: absolute;
width: calc(50% + 420px);
right: 0;
padding-top: 5px;
}
@media (orientation: landscape) and (width > 1440px) {
grid-template-columns: minmax(300px,1fr) minmax(285px,1fr) minmax(auto,3fr) minmax(auto,3fr) minmax(300px,3fr);
grid-template-rows: 0 minmax(90px, 4fr) minmax(50px, 3fr) minmax(225px, 2fr) 20px;
> .orderbook {
grid-column: 1/2;
grid-row: 3/6;
margin-top: -8px;
}
> .marketinfo {
grid-column: 3/5;
grid-row: 1/1;
}
> .chart {
grid-column: 2/6;
grid-row: 2/4;
}
> .order {
grid-column: 2/3;
grid-row: 4/5;
}
> .markets {
grid-column: 5/6;
grid-row: 4/6;
}
> .trades, #market-history {
grid-column: 1/2;
grid-row: 2/3;
}
> .usertrades {
grid-column: 4/5;
grid-row: 4/6;
}
> .userorders {
grid-column: 3/4;
grid-row: 4/6;
}
> .chart {
> #klinechartcontainer > div > div:first-child > div:first-child {
/* top: calc(7vh + 30px) !important; */
}
> .chartbuttonscontainer {
flex-direction: row;
/* margin-top: 0; */
padding-left: 10%;
margin-left: -250px;
padding-left: 12%;
}
}
}
@media ((orientation: portrait) and (width >= 980px) ) or ((width <= 1440px) and (height <=1400px)){
grid-template-columns: minmax(290px,3fr) minmax(310px,3fr) minmax(auto,6fr);
grid-template-rows: 5px minmax(50px, 6fr) minmax(240px, 1fr) minmax(100px, 1fr) minmax(100px, 1fr) 20px;
> .orderbook {
grid-column: 1/2;
grid-row: 3/6;
}
> .marketinfo {
grid-column: 1/3;
grid-row: 1/1;
}
> .chart {
grid-column: 1/4;
grid-row: 2/3;
}
> .order {
grid-column: 2/3;
grid-row: 3/4;
}
> .markets {
grid-column: 3/4;
grid-row: 5/6;
}
> .trades {
grid-column: 2/3;
grid-row: 4/6;
}
> .usertrades {
grid-column: 3/4;
grid-row: 4/5;
}
> .userorders {
grid-column: 3/4;
grid-row: 3/4;
}
> .chart {
> #klinechartcontainer > div > div:first-child > div:first-child {
padding-left: 20px !important;
}
> .chartbuttonscontainer {
}
}
}
@media (min-width: 761px) and (max-width: 980px), (min-width: 590px) and (max-width: 761px) {
grid-template-rows: minmax(50px, 1fr) minmax(250px, 9fr) minmax(225px, 2fr) minmax(240px, 3fr) auto minmax(100px, 2fr) minmax(100px, 2fr) minmax(125px, 1fr) !important;
padding-bottom: 60px;
> .trades {
display: none
}
> .chart {
> #klinechartcontainer > div > div:first-child > div:first-child {
padding-left: 20px !important;
}
> .chartbuttonscontainer {
}
}
}
}
`.innerHTML).catch(console.error);
documentInteractive.then(async ()=>{
const rnav = html`
<ul id="rnav">
<style>
#navbar {
ul#lnav {
li {
&:has(a[href='/markets']) {
display: none;
}
}
}
ul#rnav {
width: calc(100% - 40px);
display: flex;
justify-content: space-between;
[name=left] ul {
@media (max-width: 980px) {
display: none;
}
}
[name=right] ul {
@media (max-width: 592px) {
display: none;
}
}
}
> div {
position:unset !important;
margin-left:0px !important;
}
}
</style>
<li name='left'>
<ul>
<!--
Markets/Cryptots the user has visited are added here as navbar shortcuts
after any discovered USDT markets from the /account/balances table.
Sorted by users USD balance of that Crypto in descending order.
+
-->
</ul>
</li>
<li name='right'>
<ul>
<li>
<a class="" href="/account/orders">
<span class="fa fa-list-alt fa-lg fa-fw"></span>
<span style="">Orders</span>
</a>
</li>
<li>
<a class="" href="/account/history/deposits">
<span class="fa fa-cloud-download fa-lg fa-fw"></span>
<span style="">Deposits</span>
</a>
</li>
<li>
<a class="" href="/account/balances">
<span class="fa fa-money fa-lg fa-fw"></span>
<span style="">Balances</span>
</a>
</li>
</ul>
</li>
</ul>`;
document.getElementById('lnav').insertAdjacentElement('afterend', rnav);
const shortcuts = await storage.shortcuts();
let spaceAvailable = (rnav.clientWidth*0.8) - rnav.children.right.clientWidth - rnav.children.left.clientWidth;
for(let sym of shortcuts.valid) {
if (spaceAvailable < 0) { break; }
let li = html`<li><a href="/exchange/${sym}">${sym.split('-').shift()}</a></li>`;
rnav.children.left.firstElementChild.append(li);
spaceAvailable -= li.clientWidth;
}
}).catch(console.error);
if (location.pathname.startsWith('/account/balances')) {
documentInteractive.then(()=>{
const table = /**@type {HTMLTableElement} */(document.querySelector('table#coins'));
if (!table) { return; }
betterTable();
new MutationObserver(betterTable).observe(table, { childList: true, subtree: true });
async function betterTable() {
const shortcuts = await storage.shortcuts();
addUSDTMarketLinksToTable(table, shortcuts);
updateNavBarShortcuts(table, shortcuts);
}
function addUSDTMarketLinksToTable(/**@type {HTMLTableElement} */table, /**@type {Awaited<ReturnType<typeof storage['shortcuts']>>} */shortcuts) {
const symbolIndex = Array.from(table.querySelectorAll('thead > tr > th')).findIndex(x=>x.textContent.includes('Symbol'));
const symbolColumns = table.querySelectorAll(`tbody > tr > td:nth-child(${symbolIndex+1}):not(:has(a))`);
for (let td of symbolColumns) {
let sym = td.textContent;
let exch = sym + '-USDT';
if (!shortcuts.invalid.includes(exch)) {
td.classList.add('usdt_link');
td.innerHTML = `<a name="${exch}" href="/exchange/${exch}">${sym}</a>`;
}
}
}
function updateNavBarShortcuts(/**@type {HTMLTableElement} */table, /**@type {Awaited<ReturnType<typeof storage['shortcuts']>>} */shortcuts) {
const isSortedByUSD = table?.querySelector('thead > tr > th.sorting_desc[aria-label^="Est. USD"]');
if (!isSortedByUSD) { return; }
const isFullTable = table.rows.length > 15;
const isPageOne = document.querySelector('#coins_wrapper #coins_paginate ul.pagination .page-item.active:has(a[data-dt-idx="1"])');
for(let rows = table.rows, len = rows.length, i = 1, count = 0; i < len; i++) {
let tr = rows[i];
let currency = tr.querySelector('td.usdt_link a[name]')?.getAttribute('name');
if (currency && !shortcuts.invalid.includes(currency)) {
if (isFullTable && isPageOne) {
shortcuts.valid.splice(count, 0, currency);
count++;
} else {
shortcuts.valid.pushUnique(currency);
}
}
}
shortcuts.valid = shortcuts.valid.distinct().slice(0, 15);
storage.shortcuts(shortcuts);
}
});
}
if (location.pathname.startsWith('/exchange/')) {
documentComplete.then(()=>{
var h1h2 = document.querySelectorAll('#main > #main2 > *:is(h1, h2)').map(x=>x.textContent.trim()).join('|');
if (h1h2 == 'Error|Exchange not found') {
// Potential USDT markets are automatically added from the /account/balances table
// If we now find a USDT market doesn't exist for the crypto, remove & mark it as invalid
return storage.shortcuts().then((shortcuts)=>{
const coin = location.pathname.split('/').pop();
if (shortcuts.valid.removeItem(coin)) {
shortcuts.invalid.pushUnique(coin);
return storage.shortcuts(shortcuts);
}
});
} else {
// if visiting a valid market, add it to the shortcuts
return storage.shortcuts().then((shortcuts)=>{
const coin = location.pathname.split('/').pop();
shortcuts.invalid.removeItem(coin);
shortcuts.valid.pushUnique(coin);
return storage.shortcuts(shortcuts);
});
}
}).catch(console.error);
}
if (location.pathname.startsWith('/exchange/')) {
documentComplete.then(()=>{
document.querySelector('.chart .chartbuttonscontainer .chartbutton.exbutton[data-interval="1h"]')?.click();
}).catch(console.error);
}
Object.assign(GM, { html, css, deferredPromise, documentInteractive, documentLoaded, storage });
unsafeWindow.GM = GM;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment