Compare commits

...

12 Commits

Author SHA1 Message Date
1f83b6abf9 fix: type error 2023-10-27 00:01:27 +09:00
10324d5799 feat: add api 2023-10-26 23:58:33 +09:00
ad7ab6db86 chore: version update 2023-10-26 22:12:16 +09:00
ca9dd95461 feat: add pages 2023-10-26 22:11:58 +09:00
fae9cc8154 feat: better index css 2023-10-26 22:11:26 +09:00
e6d7020fc8 fix: signal_listener 2023-10-26 22:10:47 +09:00
32c1458a9c feat: show page description 2023-10-26 22:09:44 +09:00
22fad337ae fix: cache error 2023-10-26 22:09:20 +09:00
454850c6b3 refactor: StockList 2023-07-28 17:52:07 +09:00
ea611f0cdc add 120 2023-07-28 17:27:21 +09:00
d31855785d chore: remove unused. 2023-07-25 18:18:06 +09:00
f089a04ef4 feat: animation disable when diff > 200 2023-07-25 10:55:42 +09:00
28 changed files with 668 additions and 362 deletions

3
.gitignore vendored
View File

@ -10,3 +10,6 @@ stock.db
.env.test.local
.env.production.local
.env.local
# Fresh build directory
_fresh/

View File

@ -1,3 +1,5 @@
# Stock
[![Made with Fresh](https://fresh.deno.dev/fresh-badge.svg)](https://fresh.deno.dev)
주식 데이터 수집 및 선별하는 파이썬 코드입니다.

24
app.py
View File

@ -1,24 +0,0 @@
import flask
import argparse
parser = argparse.ArgumentParser(description="Stock web server")
parser.add_argument("--port", type=int, default=12001, help="port number")
parser.add_argument("--host", type=str, default="0.0.0.0", help="host address")
parser.add_argument("--debug", action="store_true", help="debug mode")
app = flask.Flask(__name__)
app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 0
@app.route("/dist/<m>")
def distServe(m:str):
return flask.send_from_directory("dist", m)
@app.route("/")
def index():
import pages
return flask.render_template("index.html", pages = pages.GenLists)
if __name__ == '__main__':
args = parser.parse_args()
app.run(host=args.host, port=args.port, debug=args.debug)

11
db/db.ts Normal file
View File

@ -0,0 +1,11 @@
import { Kysely, ParseJSONResultsPlugin } from "kysely";
import { DB as Sqlite } from "sqlite";
import { DenoSqliteDialect } from "./deno-sqlite-dialect.ts";
import { Database } from "./type.ts";
export const db = new Kysely<Database>({
dialect: new DenoSqliteDialect({
database: new Sqlite("stock.db")
}),
plugins: [new ParseJSONResultsPlugin()]
});

View File

@ -0,0 +1,27 @@
/// The MIT License (MIT)
/// Copyright (c) 2023 Alex Gleason
/// Copyright (c) 2022 Sami Koskimäki
/// https://gitlab.com/soapbox-pub/kysely-deno-sqlite
import type { SqliteDialectConfig } from 'kysely';
/** Type compatible with both [dyedgreen/deno-sqlite](https://github.com/dyedgreen/deno-sqlite) and [denodrivers/sqlite3](https://github.com/denodrivers/sqlite3). */
type DenoSqlite =
& {
close(): void;
changes: number;
lastInsertRowId: number;
}
& ({
queryEntries(sql: string, params: any): unknown[];
} | {
prepare(sql: string): {
all(...params: any): unknown[];
};
});
interface DenoSqliteDialectConfig extends Omit<SqliteDialectConfig, 'database'> {
database: DenoSqlite | (() => Promise<DenoSqlite>);
}
export type { DenoSqlite, DenoSqliteDialectConfig };

49
db/deno-sqlite-dialect.ts Normal file
View File

@ -0,0 +1,49 @@
/// The MIT License (MIT)
/// Copyright (c) 2023 Alex Gleason
/// Copyright (c) 2022 Sami Koskimäki
/// https://gitlab.com/soapbox-pub/kysely-deno-sqlite
import {
type DatabaseIntrospector,
type Dialect,
type DialectAdapter,
type Driver,
Kysely,
type QueryCompiler,
SqliteAdapter,
SqliteIntrospector,
SqliteQueryCompiler,
} from 'kysely';
import { DenoSqliteDriver } from './kysely-sqlite-driver.ts';
import type { DenoSqliteDialectConfig } from './deno-sqlite-dialect-config.ts';
class DenoSqliteDialect implements Dialect {
readonly #config: DenoSqliteDialectConfig;
constructor(config: DenoSqliteDialectConfig) {
this.#config = Object.freeze({ ...config });
}
createDriver(): Driver {
return new DenoSqliteDriver(this.#config);
}
createQueryCompiler(): QueryCompiler {
return new SqliteQueryCompiler();
}
createAdapter(): DialectAdapter {
return new SqliteAdapter();
}
// deno-lint-ignore no-explicit-any
createIntrospector(db: Kysely<any>): DatabaseIntrospector {
return new SqliteIntrospector(db);
}
}
export { DenoSqliteDialect };

112
db/kysely-sqlite-driver.ts Normal file
View File

@ -0,0 +1,112 @@
/// The MIT License (MIT)
/// Copyright (c) 2023 Alex Gleason
/// Copyright (c) 2022 Sami Koskimäki
/// https://gitlab.com/soapbox-pub/kysely-deno-sqlite
import { CompiledQuery, type DatabaseConnection, type Driver, type QueryResult } from 'kysely';
import type { DenoSqlite, DenoSqliteDialectConfig } from './deno-sqlite-dialect-config.ts';
class DenoSqliteDriver implements Driver {
readonly #config: DenoSqliteDialectConfig;
readonly #connectionMutex = new ConnectionMutex();
#db?: DenoSqlite;
#connection?: DatabaseConnection;
constructor(config: DenoSqliteDialectConfig) {
this.#config = Object.freeze({ ...config });
}
async init(): Promise<void> {
this.#db = typeof this.#config.database === 'function' ? await this.#config.database() : this.#config.database;
this.#connection = new DenoSqliteConnection(this.#db);
if (this.#config.onCreateConnection) {
await this.#config.onCreateConnection(this.#connection);
}
}
async acquireConnection(): Promise<DatabaseConnection> {
// SQLite only has one single connection. We use a mutex here to wait
// until the single connection has been released.
await this.#connectionMutex.lock();
return this.#connection!;
}
async beginTransaction(connection: DatabaseConnection): Promise<void> {
await connection.executeQuery(CompiledQuery.raw('begin'));
}
async commitTransaction(connection: DatabaseConnection): Promise<void> {
await connection.executeQuery(CompiledQuery.raw('commit'));
}
async rollbackTransaction(connection: DatabaseConnection): Promise<void> {
await connection.executeQuery(CompiledQuery.raw('rollback'));
}
// deno-lint-ignore require-await
async releaseConnection(): Promise<void> {
this.#connectionMutex.unlock();
}
// deno-lint-ignore require-await
async destroy(): Promise<void> {
this.#db?.close();
}
}
class DenoSqliteConnection implements DatabaseConnection {
readonly #db: DenoSqlite;
constructor(db: DenoSqlite) {
this.#db = db;
}
executeQuery<O>({ sql, parameters }: CompiledQuery): Promise<QueryResult<O>> {
const rows = 'queryEntries' in this.#db
? this.#db.queryEntries(sql, parameters)
: this.#db.prepare(sql).all(...parameters);
const { changes, lastInsertRowId } = this.#db;
return Promise.resolve({
rows: rows as O[],
numAffectedRows: BigInt(changes),
insertId: BigInt(lastInsertRowId),
});
}
// deno-lint-ignore require-yield
async *streamQuery<R>(): AsyncIterableIterator<QueryResult<R>> {
throw new Error('Sqlite driver doesn\'t support streaming');
}
}
class ConnectionMutex {
#promise?: Promise<void>;
#resolve?: () => void;
async lock(): Promise<void> {
while (this.#promise) {
await this.#promise;
}
this.#promise = new Promise((resolve) => {
this.#resolve = resolve;
});
}
unlock(): void {
const resolve = this.#resolve;
this.#promise = undefined;
this.#resolve = undefined;
resolve?.();
}
}
export { DenoSqliteDriver };

64
db/type.ts Normal file
View File

@ -0,0 +1,64 @@
import { ColumnType, Generated, Insertable, Selectable, Updateable } from "kysely";
/**
* "Code" TEXT,
"Date" TEXT,
"Close" INTEGER NOT NULL,
"Diff" INTEGER NOT NULL,
"Open" INTEGER NOT NULL,
"High" INTEGER NOT NULL,
"Low" INTEGER NOT NULL,
"Volume" INTEGER NOT NULL,
*/
export interface StockTable {
Code: string;
Date: string;
Close: number;
Diff: number;
Open: number;
High: number;
Low: number;
Volume: number;
}
export interface KRXCorpTable{
Name: string;
/**
* PK
*/
Code: string;
Sector: string;
Product: string;
ListingDay: string;
ClosingMonth: string;
Representative: string;
Homepage: string;
AddressArea: string;
LastUpdate: string;
}
export interface KOSPITable{
Name: string;
/**
* PK
*/
Code: string;
}
export interface KOSDAQTable{
Name: string;
/**
* PK
*/
Code: string;
}
export interface Database {
stock: StockTable;
KRXCorp: KRXCorpTable;
KOSPI: KOSPITable;
KOSDAQ: KOSDAQTable;
}

View File

@ -2,31 +2,28 @@
"lock": false,
"tasks": {
"start": "deno run -A --watch=static/,routes/ dev.ts",
"update": "deno run -A -r https://fresh.deno.dev/update ."
},
"lint": {
"rules": {
"tags": [
"fresh",
"recommended"
]
}
"prod_start": "deno run -A main.ts",
"update": "deno run -A -r https://fresh.deno.dev/update .",
"build": "deno run -A dev.ts build",
"preview": "deno run -A main.ts"
},
"lint": { "rules": { "tags": ["fresh", "recommended"] } },
"imports": {
"$fresh/": "https://deno.land/x/fresh@1.3.1/",
"preact": "https://esm.sh/preact@10.15.1",
"preact/": "https://esm.sh/preact@10.15.1/",
"preact-render-to-string": "https://esm.sh/*preact-render-to-string@6.2.0",
"@preact/signals": "https://esm.sh/*@preact/signals@1.1.3",
"@preact/signals-core": "https://esm.sh/*@preact/signals-core@1.2.3",
"$fresh/": "https://deno.land/x/fresh@1.5.2/",
"preact": "https://esm.sh/preact@10.18.1",
"preact/": "https://esm.sh/preact@10.18.1/",
"preact-render-to-string": "https://esm.sh/*preact-render-to-string@6.2.2",
"@preact/signals": "https://esm.sh/*@preact/signals@1.2.1",
"@preact/signals-core": "https://esm.sh/*@preact/signals-core@1.5.0",
"auto-animate": "https://esm.sh/@formkit/auto-animate@0.7.0",
"auto-animate/": "https://esm.sh/@formkit/auto-animate@0.7.0/",
"twind": "https://esm.sh/twind@0.16.19",
"twind/": "https://esm.sh/twind@0.16.19/",
"$std/": "https://deno.land/std@0.193.0/"
"$std/": "https://deno.land/std@0.203.0/",
"kysely": "npm:kysely@^0.26.3",
"kysely/helpers/sqlite": "npm:kysely@^0.26.3/helpers/sqlite",
"sqlite": "https://deno.land/x/sqlite@v3.8/mod.ts"
},
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "preact"
}
"compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "preact" },
"exclude": ["**/_fresh/*"]
}

View File

@ -1,36 +1,42 @@
// DO NOT EDIT. This file is generated by fresh.
// DO NOT EDIT. This file is generated by Fresh.
// This file SHOULD be checked into source version control.
// This file is automatically updated during development when running `dev.ts`.
import * as $0 from "./routes/_404.tsx";
import * as $1 from "./routes/_app.tsx";
import * as $2 from "./routes/api/joke.ts";
import * as $3 from "./routes/api/kosdaq.ts";
import * as $4 from "./routes/api/kospi.ts";
import * as $5 from "./routes/api/pages/[name].ts";
import * as $6 from "./routes/api/pages/index.ts";
import * as $7 from "./routes/greet/[name].tsx";
import * as $8 from "./routes/index.tsx";
import * as $9 from "./routes/pages/[name].tsx";
import * as $2 from "./routes/api/corps/[index].ts";
import * as $3 from "./routes/api/corps/index.ts";
import * as $4 from "./routes/api/joke.ts";
import * as $5 from "./routes/api/kosdaq.ts";
import * as $6 from "./routes/api/kospi.ts";
import * as $7 from "./routes/api/pages/[name].ts";
import * as $8 from "./routes/api/pages/index.ts";
import * as $9 from "./routes/greet/[name].tsx";
import * as $10 from "./routes/index.tsx";
import * as $11 from "./routes/pages/[name].tsx";
import * as $$0 from "./islands/Counter.tsx";
import * as $$1 from "./islands/StockList.tsx";
import * as $$1 from "./islands/Search.tsx";
import * as $$2 from "./islands/StockList.tsx";
const manifest = {
routes: {
"./routes/_404.tsx": $0,
"./routes/_app.tsx": $1,
"./routes/api/joke.ts": $2,
"./routes/api/kosdaq.ts": $3,
"./routes/api/kospi.ts": $4,
"./routes/api/pages/[name].ts": $5,
"./routes/api/pages/index.ts": $6,
"./routes/greet/[name].tsx": $7,
"./routes/index.tsx": $8,
"./routes/pages/[name].tsx": $9,
"./routes/api/corps/[index].ts": $2,
"./routes/api/corps/index.ts": $3,
"./routes/api/joke.ts": $4,
"./routes/api/kosdaq.ts": $5,
"./routes/api/kospi.ts": $6,
"./routes/api/pages/[name].ts": $7,
"./routes/api/pages/index.ts": $8,
"./routes/greet/[name].tsx": $9,
"./routes/index.tsx": $10,
"./routes/pages/[name].tsx": $11,
},
islands: {
"./islands/Counter.tsx": $$0,
"./islands/StockList.tsx": $$1,
"./islands/Search.tsx": $$1,
"./islands/StockList.tsx": $$2,
},
baseUrl: import.meta.url,
};

53
gen.py
View File

@ -5,7 +5,6 @@ import sqlite3
from typing import Dict, List
from render import *
import db as database
from jinja2 import Environment, PackageLoader, select_autoescape
import pandas as pd
import tqdm
@ -92,7 +91,7 @@ def isMACDCrossSignal(signal: pd.Series, macd: pd.Series, nday: int, order=1) ->
signal.iloc[nday+order] < macd.iloc[nday+order])
def isRelativeDiffLessThan(a:pd.Series,b:pd.Series, threshold: float,nday:int) -> bool:
return abs(a.iloc[nday] - b.iloc[nday]) / b.iloc[nday] < threshold
return abs(a.iloc[nday] - b.iloc[nday + 1]) / b.iloc[nday + 1] < threshold
def isDiffGreaterThan(a:pd.Series,b:pd.Series, nday:int) -> bool:
"""a is bigger than b"""
@ -145,7 +144,6 @@ def collect(data: DataStore, collector: OutputCollector, corp: database.KRXCorp
d5 = d(5)
d20 = d(20)
d25 = d(25)
d30 = d(30)
d45 = d(45)
d60 = d(60)
d120 = d(120)
@ -156,8 +154,14 @@ def collect(data: DataStore, collector: OutputCollector, corp: database.KRXCorp
bollinger_upperband = d25 + 2* d_std25
a = [d5, d20, d45, d60]
a = [d5, d20, d45]
for nday in ndays:
if openv[nday] <= d240[nday] and d240[nday] <= close[nday] and d240[nday + 1] < d240[nday]:
collector.collect("양봉사이240일선증가", corp, stock.index[nday])
if d5[nday + 1] < d5[nday] and d5[nday + 2] > d5[nday + 1] and d20[nday + 1] < d20[nday]:
collector.collect("5일선반등120선증가", corp, stock.index[nday])
if openv[nday] <= d20[nday] and d20[nday] <= close[nday]:
collector.collect("양봉사이20일선", corp, stock.index[nday])
@ -207,6 +211,9 @@ def collect(data: DataStore, collector: OutputCollector, corp: database.KRXCorp
if (D240BiggerThanYesterDay):
collector.collect("240일 증가", corp, stock.index[nday])
if max([d[nday] for d in (d20, d60, d120)]) < min(close[nday], openv[nday]):
collector.collect("떠있음", corp, stock.index[nday])
if (d60[nday + 1] < d60[nday]):
collector.collect("정배열60", corp, stock.index[nday])
if (d20[nday + 1] < d20[nday]):
@ -217,6 +224,11 @@ def collect(data: DataStore, collector: OutputCollector, corp: database.KRXCorp
d120[nday + 1] <= d120[nday]):
collector.collect("모두 정배열", corp, stock.index[nday])
if(d120[nday + 1] <= d120[nday] and
d120[nday + 1] < d240[nday] and
d120[nday] >= d240[nday]):
collector.collect("120선240선추월", corp, stock.index[nday])
if (d5[nday + 1] < d20[nday + 1] and d20[nday] < d5[nday]):
collector.collect("d20d5돌파", corp, stock.index[nday])
@ -248,8 +260,6 @@ def collect(data: DataStore, collector: OutputCollector, corp: database.KRXCorp
#rsi_signal = macd.loc[::-1].ewm(span=7).mean().loc[::-1]
parser = argparse.ArgumentParser(description="주식 검색 정보를 출력합니다.")
parser.add_argument("--format", "-f", choices=["json", "html"], default="html",
help="출력 포맷을 지정합니다. 기본값은 html입니다.")
parser.add_argument("--dir", "-d", default=".", help="출력할 폴더를 지정합니다.")
parser.add_argument("--corp", "-c", help="주식 코드를 지정합니다. 지정하지 않으면 kosdaq과 kospi만 검색합니다.")
parser.add_argument("--fullSearch", help="모든 주식을 검색합니다.", action='store_true')
@ -268,10 +278,6 @@ if __name__ == "__main__":
if args.corp:
krx_corps = [corp for corp in krx_corps if corp.Code == args.corp]
env = Environment(
loader=PackageLoader('render', 'templates'),
autoescape=select_autoescape(['html', 'xml'])
)
collector = OutputCollector()
prepareCollector(collector)
@ -281,25 +287,10 @@ if __name__ == "__main__":
dataStore.clearCache()
for k,v in collector.data.items():
if args.format == "json":
data = json.dumps(v.toDict(), indent=4, ensure_ascii=False)
if args.printStdout:
print(k)
print(data)
else:
with open(os.path.join(args.dir, k + ".json"), "w", encoding="UTF-8") as f:
f.write(data)
data = json.dumps(v.toDict(), ensure_ascii=False)
if args.printStdout:
print(k)
print(data)
else:
template = env.get_template("Lists.html")
days = v.corpListByDate.keys()
days = list(days)
days.sort(reverse=True)
days = days[:5]
html = template.render(collected=v, title=k, days=days, lastUpdate=datetime.date.today().isoformat())
if args.printStdout:
print(html)
else:
with open(os.path.join(args.dir, k + ".html"), "w", encoding="UTF-8") as f:
f.write(html)
with open(os.path.join(args.dir, k + ".json"), "w", encoding="UTF-8") as f:
f.write(data)

5
islands/Search.tsx Normal file
View File

@ -0,0 +1,5 @@
export default function Search(){
return <div>
<div>div</div>
</div>
}

View File

@ -1,9 +1,18 @@
import { Button } from "../components/Button.tsx";
import { useEffect, useRef } from "preact/hooks";
import { useEffect, useLayoutEffect, useRef } from "preact/hooks";
import { ComponentChildren } from "preact";
import { Signal, useSignal } from "@preact/signals";
import { IS_BROWSER } from "$fresh/runtime.ts";
import { mapValues } from "$std/collections/map_values.ts";
import { useAsync } from "../util/util.ts";
import {
Coperation,
CorpSimple,
fetchKosdaqList,
fetchKospiList,
fetchPageInfo,
PageCorpsInfo,
} from "../util/api.ts";
interface StockProps {
pageName: string;
@ -31,94 +40,70 @@ function ToggleButton(props: ToggleButtonProps) {
);
}
type QueryStatus<T> = {
type: "loading";
} | {
type: "complete";
data: T;
} | {
type: "error";
err: Error;
};
function useAsync<T>(fn: () => Promise<T>): Signal<QueryStatus<T>> {
const state = useSignal({
type: "loading",
} as QueryStatus<T>);
function StockListByDate(
{ prevSet, rows, name }: {
prevSet: Set<string>;
rows: Coperation[];
name: string;
},
) {
const lastCount = useRef(rows.length);
const curCount = rows.length;
const parent = useRef<HTMLDivElement>(null);
const controller = useRef<
{
isEnabled: () => boolean;
disable: () => void;
enable: () => void;
} | undefined
>();
useEffect(() => {
(async () => {
try {
const data = await fn();
state.value = {
type: "complete",
data: data,
};
} catch (err) {
state.value = {
type: "error",
err: err,
};
console.log("animation mount on ", name);
const { default: autoAnimate } = await import(
"https://esm.sh/@formkit/auto-animate@0.7.0"
);
if (parent.current) {
const cntr = autoAnimate(parent.current);
controller.current = cntr;
}
})();
}, []);
return state;
}
}, [parent]);
interface Coperation {
Name: string;
Code: string;
Sector: string;
Product: string;
ListingDay: string;
ClosingMonth: string;
Representative: string;
Homepage: string;
AddressArea: string;
LastUpdate: string;
}
useLayoutEffect(() => {
if (controller.current) {
if (Math.abs(curCount - lastCount.current) > 200) {
console.log("disable animation", curCount, "from", lastCount.current);
controller.current.disable();
} else {
console.log("enable animation", curCount, "from", lastCount.current);
controller.current.enable();
}
lastCount.current = curCount;
}
}, [parent, rows]);
interface PageCorpsInfo {
name: string;
description: string;
corpListByDate: Record<string, Coperation[]>;
}
interface CorpSimple {
code: string;
name: string;
}
function StockListByDate({prevSet, rows, name}:{prevSet:Set<string>,
rows:Coperation[],
name: string}){
const parent = useRef<HTMLDivElement>(null);
useEffect(()=>{
(async ()=>{
console.log("animation mount on ",name);
const {default:autoAnimate} = await import("https://esm.sh/@formkit/auto-animate@0.7.0");
parent.current && autoAnimate(parent.current)
})();
},[parent]);
return <div ref={parent}>
<h2 class="text-lg">{name}</h2>
{rows.map((row) => {
const firstOccur = !prevSet.has(row.Code);
return (
<div
key={row.Code}
class={[
"bg-white",
firstOccur ? "text-[#ff5454]" : "text-black",
].join(" ")}
>
<a href={`https://stockplus.com/m/stocks/KOREA-A${row.Code}`}>
{row.Name}
</a>
</div>
);
})}
</div>
return (
<div ref={parent}>
<h2 class="text-lg">{name}</h2>
{rows.map((row) => {
const firstOccur = !prevSet.has(row.Code);
return (
<div
key={row.Code}
class={[
"bg-white",
firstOccur ? "text-[#ff5454] underline" : "text-black",
].join(" ")}
>
<a href={`https://stockplus.com/m/stocks/KOREA-A${row.Code}`}>
{row.Name}
</a>
</div>
);
})}
</div>
);
}
function StockList({ data }: { data: PageCorpsInfo }) {
@ -135,14 +120,13 @@ function StockList({ data }: { data: PageCorpsInfo }) {
const prevSet = i == 0 ? new Set<string>() : sets[i - 1];
const rows = corpListByDate[x];
return (
<StockListByDate key={x} name={x} prevSet={prevSet} rows={rows}></StockListByDate>
<StockListByDate key={x} name={x} prevSet={prevSet} rows={rows} />
);
})}
</div>
);
}
type FilterInfoOption = {
list: {
items: CorpSimple[];
@ -155,7 +139,7 @@ function filterInfo(info: Coperation[], filterList: FilterInfoOption) {
const checkMap = new Map<string, boolean>();
for (const l of filterList.list) {
for (const i of l.items) {
checkMap.set(i.code, l.include);
checkMap.set(i.Code, l.include);
}
}
return info.filter((x) => {
@ -169,17 +153,13 @@ function filterInfo(info: Coperation[], filterList: FilterInfoOption) {
}
export default function StockListUI(props: StockProps) {
const sig = useAsync<[PageCorpsInfo, CorpSimple[], CorpSimple[]]>(async () => {
const res = await Promise.all([
fetch("/api/pages/" + encodeURIComponent(props.pageName)),
fetch("/api/kospi"),
fetch("/api/kosdaq"),
]);
const corpsInfo = await res[0].json() as PageCorpsInfo;
const kospi = await res[1].json();
const kosdaq = await res[2].json();
return [corpsInfo, kospi, kosdaq];
});
const sig = useAsync<[PageCorpsInfo, CorpSimple[], CorpSimple[]]>(() =>
Promise.all([
fetchPageInfo(props.pageName),
fetchKospiList(),
fetchKosdaqList(),
])
);
const viewKospi = useSignal(true);
const viewKosdaq = useSignal(false);
const viewOtherwise = useSignal(false);
@ -203,34 +183,47 @@ export default function StockListUI(props: StockProps) {
<p>File Loading Failed</p>
</div>
)
: <StockList data={applyFilter(sig.value.data[0], sig.value.data[1], sig.value.data[2])}></StockList>}
: (
<StockList
data={applyFilter(
sig.value.data[0],
sig.value.data[1],
sig.value.data[2],
)}
/>
)}
</div>
)}
</div>
</div>
);
function applyFilter(data: PageCorpsInfo, kospi: CorpSimple[], kosdaq: CorpSimple[]): PageCorpsInfo{
const filter = getFilters(kospi,kosdaq);
function applyFilter(
data: PageCorpsInfo,
kospi: CorpSimple[],
kosdaq: CorpSimple[],
): PageCorpsInfo {
const filter = getFilters(kospi, kosdaq);
return {
name: data.name,
description: data.description,
corpListByDate: mapValues(data.corpListByDate, (it: Coperation[])=>{
corpListByDate: mapValues(data.corpListByDate, (it: Coperation[]) => {
return filterInfo(it, filter);
})
}
}),
};
}
function getFilters(kospi: CorpSimple[], kosdaq: CorpSimple[]): FilterInfoOption{
function getFilters(
kospi: CorpSimple[],
kosdaq: CorpSimple[],
): FilterInfoOption {
return {
otherwise: viewOtherwise.value,
list: [{
include: viewKospi.value,
items: kospi
},
{
include: viewKosdaq.value,
items: kosdaq
}
]
}
include: viewKospi.value,
items: kospi,
}, {
include: viewKosdaq.value,
items: kosdaq,
}],
};
}
}

View File

@ -9,9 +9,15 @@ import "$std/dotenv/load.ts";
import { start } from "$fresh/server.ts";
import manifest from "./fresh.gen.ts";
import twindPlugin from "$fresh/plugins/twind.ts";
import twindPlugin from "$fresh/plugins/twindv1.ts";
import twindConfig from "./twind.config.ts";
console.log("start");
Deno.addSignalListener("SIGINT", () => {
Deno.exit(0);
});
await start(manifest, {
port: 12001,
plugins: [twindPlugin(twindConfig)]

View File

@ -26,18 +26,25 @@ function watchFile(
}
}
})();
Deno.addSignalListener("SIGINT", () => {
const closeHandler = () => {
watcherRef.close();
});
};
Deno.addSignalListener("SIGINT", closeHandler);
return ()=>{
Deno.removeSignalListener("SIGINT", closeHandler);
closeHandler();
}
}
let pages_meta: PageDescription[] = [];
let mtime = 0;
let lastest_disposer = () => {};
export async function get_pages_meta(): Promise<[PageDescription[],number]>{
if (pages_meta) {
if (pages_meta.length == 0) {
pages_meta = await readPagesDescription();
mtime = Date.now();
watchFile(PAGES_PATH, async () => {
lastest_disposer();
lastest_disposer = watchFile(PAGES_PATH, async () => {
pages_meta = await readPagesDescription();
mtime = Date.now();
});

View File

@ -36,11 +36,11 @@
macd가 아래로 내려가는 시점을 찾습니다. macd 는 5일선과 10일선으로 이루어지고
시그널을 구하기 위한 이동 평균은 4일입니다.
- name: 뭉침
description: d5, d20, d45, d60 만난것 종가 5% 이내
description: d5, d20, d45 만난것 종가 5% 이내
- name: 뭉침01
description: d5, d20, d45, d60 만난것 종가 1% 이내
description: d5, d20, d45 만난것 종가 1% 이내
- name: 뭉침03
description: d5, d20, d45, d60 만난것 종가 3% 이내
description: d5, d20, d45 만난것 종가 3% 이내
- name: 45일선 반등
description: 45일 선반등
@ -64,3 +64,13 @@
description: '볼린저 밴드(25일선 ,표준편차 2배)의 위 밴드 값을 넘었을 때 표시.'
- name: 양봉사이20일선
description: Open과 Close 사이 20일 선
- name: 양봉사이240일선증가
description: Open과 Close 사이 240일 선. 240일 선 증가
- name: 떠있음
description: |
양봉, 음봉이 20일선, 60일선, 120선보다 떠있으면
- name: 5일선반등120선증가
description: 5일선이 반등 120 선 증가
- name: 120선240선추월
description: |
120선이 상승해서 240일 선을 뚫을때

View File

@ -0,0 +1,35 @@
import { Handlers } from "$fresh/server.ts";
import { db } from "../../../db/db.ts";
import { jsonArrayFrom } from "kysely/helpers/sqlite";
export const handler: Handlers = {
async GET(req, ctx): Promise<Response> {
const headers = new Headers({
"content-type": "application/json"
});
const index = ctx.params.index;
const corp = await db.selectFrom("KRXCorp")
.selectAll([
"KRXCorp"
])
.select(eb=> [
jsonArrayFrom(eb.selectFrom("stock")
.select([
"stock.Close",
"stock.Open",
"stock.Low",
"stock.High",
"stock.Date",
"stock.Volume",
])
.where("Code", "=", index)
.orderBy("Date", "desc")
.limit(100)
).as("prices")]
)
.where("Code", "=", index)
.executeTakeFirst();
return new Response(JSON.stringify(corp ?? null), {headers});
},
}

21
routes/api/corps/index.ts Normal file
View File

@ -0,0 +1,21 @@
import { Handlers } from "$fresh/server.ts";
import { db } from "../../../db/db.ts";
export const handler: Handlers = {
async GET(req, _ctx): Promise<Response> {
const headers = new Headers({
"content-type": "application/json"
});
const url = new URL(req.url);
const q = url.searchParams.get("q");
const name = url.searchParams.get("name");
const corps = await db.selectFrom("KRXCorp")
.selectAll([
"KRXCorp"
])
.$if(!!q, qb=> qb.where("Name", "like", "%"+q+"%"))
.$if(!!name, qb => qb.where("Name", "=", name))
.execute();
return new Response(JSON.stringify(corps), {headers});
},
}

View File

@ -1,17 +1,17 @@
import { Handlers } from "$fresh/server.ts";
import {DB} from "https://deno.land/x/sqlite/mod.ts";
import { db } from "../../db/db.ts";
export const handler: Handlers = {
async GET(req, _ctx): Promise<Response> {
const headers = new Headers({
"content-type": "application/json"
});
const db = new DB("stock.db");
const conn = db.query("SELECT Code,Name FROM KOSDAQ");
const body = conn.map(row=>({
code: row[0],
name: row[1]
}))
return new Response(JSON.stringify(body), {headers});
const rows = await db.selectFrom("KOSDAQ")
.select([
"Code",
"Name"
])
.execute();
return new Response(JSON.stringify(rows), {headers});
},
}

View File

@ -1,17 +1,17 @@
import { Handlers } from "$fresh/server.ts";
import {DB} from "https://deno.land/x/sqlite/mod.ts";
import { db } from "../../db/db.ts";
export const handler: Handlers = {
async GET(req, _ctx): Promise<Response> {
const headers = new Headers({
"content-type": "application/json"
});
const db = new DB("stock.db");
const conn = db.query("SELECT Code,Name FROM KOSPI");
const body = conn.map(row=>({
code: row[0],
name: row[1]
}))
return new Response(JSON.stringify(body), {headers});
const rows = await db.selectFrom("KOSPI")
.select([
"Code",
"Name"
])
.execute();
return new Response(JSON.stringify(rows), {headers});
},
}

View File

@ -25,16 +25,18 @@ export const handler: Handlers = {
const mtime = stat.mtime ?? new Date(0);
const body = await Deno.readTextFile(path);
headers.set("last-modified", mtime.toUTCString());
console.log(mtime);
// headers.set("cache-control", "max-age=600");
const ifModifiedSinceValue = req.headers.get("if-modified-since");
if ( ifModifiedSinceValue &&
mtime.getTime() != new Date(ifModifiedSinceValue).getTime()
){
return new Response(null, {
status: Status.NotModified,
statusText: STATUS_TEXT[Status.NotModified]
})
}
// const ifModifiedSinceValue = req.headers.get("if-modified-since");
// if ( ifModifiedSinceValue &&
// mtime.getTime() <= new Date(ifModifiedSinceValue).getTime()
// ){
// return new Response(null, {
// status: Status.NotModified,
// statusText: STATUS_TEXT[Status.NotModified]
// })
// }
return new Response(body, {headers});
},
};

View File

@ -1,25 +1,21 @@
import { Head } from "$fresh/runtime.ts";
import { useSignal } from "@preact/signals";
import {Button} from "../components/Button.tsx";
import { PageDescription, get_pages_meta } from "../pages.ts";
import { get_pages_meta, PageDescription } from "../pages.ts";
import { Handlers, PageProps } from "$fresh/server.ts";
export const handler: Handlers = {
async GET(_req, ctx){
const [pages,_] = await get_pages_meta();
async GET(_req, ctx) {
const [pages, _] = await get_pages_meta();
return await ctx.render(pages);
}
}
},
};
export default function Home({data}: PageProps<PageDescription[]>) {
const count = useSignal(3);
export default function Home({ data }: PageProps<PageDescription[]>) {
return (
<>
<Head>
<title>stock-front</title>
</Head>
<div class="px-4 py-8 mx-auto bg-[#86efac]">
<div class="px-4 py-8 mx-auto bg-[#86efac] min-h-screen">
<div class="max-w-screen-md mx-auto flex flex-col items-center justify-center">
<img
class="my-6"
@ -31,14 +27,16 @@ export default function Home({data}: PageProps<PageDescription[]>) {
<h1 class="text-4xl font-bold">Stock</h1>
<div class="my-4">
<ul>
{
data.map(x=><li class="my-2">
<a class="p-2 block hover:bg-gray-300 bg-white rounded" href={`/pages/${encodeURIComponent(x.name)}`}>
{data.map((x) => (
<li class="my-2">
<a
class="p-2 block hover:bg-gray-300 bg-white rounded"
href={`/pages/${encodeURIComponent(x.name)}`}
>
{x.name}
</a>
</li>
)
}
</li>
))}
</ul>
</div>
</div>

View File

@ -1,14 +1,26 @@
import { PageProps } from "$fresh/server.ts";
import { PageProps, Handlers } from "$fresh/server.ts";
import { Head } from "$fresh/runtime.ts";
import { get_pages_meta, PageDescription } from "../../pages.ts";
import StockList from "../../islands/StockList.tsx";
export const handler: Handlers = {
async GET(_req, ctx) {
const [pages, _] = await get_pages_meta();
const name = ctx.params.name;
const page = pages.filter(x=> x.name === name);
if (page.length === 0) {
return await ctx.renderNotFound();
}
return await ctx.render(page[0]);
},
};
export default function Pages(props: PageProps) {
export default function Pages(props: PageProps<PageDescription>) {
return <>
<Head>
<title>Stock: {props.params.name}</title>
</Head>
<div class="px-4 py-8 mx-auto bg-[#86efac]">
<div class="px-4 py-8 mx-auto bg-[#86efac] min-h-screen">
<div class="max-w-screen-md mx-auto flex flex-col items-center justify-center">
<img
class="my-6"
@ -18,6 +30,7 @@ export default function Pages(props: PageProps) {
alt="stock graph"
/>
<h1 class="text-4xl">{props.params.name}</h1>
<p>{props.data.description}</p>
<StockList pageName={props.params.name}></StockList>
</div>
</div>

View File

@ -1,60 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Stock</title>
<style>
body{
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(to right, #2b2b2b, #3d1844);
color: #fff;
}
.table_item:nth-child(2n){
background: #a7a7a7;
}
.table_item:nth-child(2n+1){
background: #fff;
}
.table_item:hover{
background: #8d8d8d;
}
.container{
display: grid;
grid-template-rows: 24px auto;
background: #f0f0f0;
color: black;
box-shadow: 0px 0px 5px 0px white;
text-decoration: none;
grid-template-columns: repeat({{ 5 }}, 1fr);
}
.container a:link, a:visited{
text-decoration: none;
color: black;
}
.data_header{
border-bottom: 1px solid #a7a7a7;
}
</style>
</head>
<body>
<div style="margin: auto; max-width: 750px;">
<h1>{{title}} Stock List</h1>
<h3>{{lastUpdate}}</h3>
<section class="description">
{{collected.description}}
</section>
<section class="container">
{% for day in days|reverse %}
<div class="data_header">{{ day }}</div>
{% endfor %}
{% for day in days|reverse %}
{% set corplist = collected.corpListByDate[day] %}
<div>{% for item in corplist %}
<div class="table_item"><a href="https://stockplus.com/m/stocks/KOREA-A{{ item.Code }}">{{ item.Name }}({{item.Code}})</a></div>{% endfor %}
</div>
{% endfor %}
</section>
</div>
</body>
</html>

View File

@ -1,40 +0,0 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Stock</title>
<style>
body{
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(to right, #2b2b2b, #3d1844);
color: #fff;
}
.container{
display: grid;
background: #f0f0f0;
color: black;
box-shadow: 0px 0px 5px 0px white;
text-decoration: none;
}
.container a:link, a:visited{
text-decoration: none;
color: black;
font-size: 40px;
}
.data_header{
border-bottom: 1px solid #a7a7a7;
}
</style>
</head>
<body>
<div style="margin: auto; max-width: 750px;">
<h1>Main</h1>
<div class="container">
{% for p in pages %}
<a href="/dist/{{p.name}}.html">{{p.name}}</a>
{% endfor %}
</div>
</div>
</body>
</html>

View File

@ -1,5 +1,10 @@
import { Options } from "$fresh/plugins/twind.ts";
import { defineConfig, Preset } from "https://esm.sh/@twind/core@1.1.3";
import presetTailwind from "https://esm.sh/@twind/preset-tailwind@1.1.4";
import presetAutoprefix from "https://esm.sh/@twind/preset-autoprefix@1.0.7";
export default {
...defineConfig({
presets: [presetTailwind() as Preset, presetAutoprefix()],
}),
selfURL: import.meta.url,
} as Options;
}

38
util/api.ts Normal file
View File

@ -0,0 +1,38 @@
export interface Coperation {
Name: string;
Code: string;
Sector: string;
Product: string;
ListingDay: string;
ClosingMonth: string;
Representative: string;
Homepage: string;
AddressArea: string;
LastUpdate: string;
}
export interface PageCorpsInfo {
name: string;
description: string;
corpListByDate: Record<string, Coperation[]>;
}
export interface CorpSimple {
Code: string;
Name: string;
}
export async function fetchPageInfo(pageName: string): Promise<PageCorpsInfo>{
const res = await fetch("/api/pages/" + encodeURIComponent(pageName));
return await res.json();
}
export async function fetchKospiList(): Promise<CorpSimple[]>{
const res = await fetch("/api/kospi");
return await res.json();
}
export async function fetchKosdaqList(): Promise<CorpSimple[]> {
const res = await fetch("/api/kosdaq");
return await res.json();
}

35
util/util.ts Normal file
View File

@ -0,0 +1,35 @@
import { Signal, useSignal } from "@preact/signals";
import { useEffect } from "preact/hooks";
export type QueryStatus<T> = {
type: "loading";
} | {
type: "complete";
data: T;
} | {
type: "error";
err: Error;
};
export function useAsync<T>(fn: () => Promise<T>): Signal<QueryStatus<T>> {
const state = useSignal({
type: "loading",
} as QueryStatus<T>);
useEffect(() => {
(async () => {
try {
const data = await fn();
state.value = {
type: "complete",
data: data,
};
} catch (err) {
state.value = {
type: "error",
err: err,
};
}
})();
}, []);
return state;
}