tl;dr
Parsing Date
Use only java.time classes for date-time work.
LocalDate start = LocalDate.parse( "2018-10-27" ); // Parse input string as a date-only value (no time-of-day, no time zone), a `java.time.LocalDate` object.
… in an if-statement and showing only results in stipulated time range
if (
( ! stockDay.getDate().isBefore( start ) ) // A shorter way to ask "is equal to OR later" is "is NOT before".
&&
stockDay.getDate().isBefore( stop ) // Half-Open approach, where beginning is *inclusive* while the ending is *exclusive*.
) { … }
Even better, represent your start-stop date range using the LocalDateRange
class found in ThreeTen-Extra library.
if (
LocalDateRange.of( start , stop )
.contains( LocalDate.parse( "2018-10-27" ) )
) { … }
Details
How the code should be amended to make it work?
You may be sorry you asked. I went a little nuts with this Answer, being curious about Candlestick Hammer, and wanting to work more with Java Streams.
Fix the date-time handling
You are using troublesome old date-time classes that were supplanted years ago but the java.time classes.
Furthermore, you are abusing those legacy classes. You are putting a date-only value in a date-with-time-of-day type. Use java.time.LocalDate
instead, for a date-only value.
You failed to define a formatting pattern to match your inputs. Your pattern says "yyyy-MM-dd"
but your inputs are in day-month-year order rather than year-month-day order. Define a formatting pattern to match your string inputs. Use the modern class, DateTimeFormatter
.
DateTimeFormatter f = DateTimeFormatter.ofPattern( "dd/MM/uuuu" ) ;
Parse your input string.
LocalDate ld = LocalDate.parse( "31/10/2016" , f ) ;
ld.toString(): 2018-10-31
Be clear that a date-time object has no “format”. Formats relate to text representing a date-time value/object, to the String
object being generated from a date-time object. But the date-time object and the String
remain separate and distinct.
By the way, do not use such a custom or localized format for your inputs. Always use standard ISO 8601 formats when exchanging date-time values as text. The java.time classes use these standard formats by default when parsing/generating strings.
Define a class for data-model
Rather than mess with arrays or a bunch of List
objects, define a class for your data. Name it something like StockDay
to represent a stock’s performance on a particular date.
Never use double
or Double
, nor float
or Float
, to represent money. These types use floating-point technology that trades away accuracy to get faster execution performance. Instead use the BigDecimal
class; slower, but accurate.
On this class we include the business logic in defining a Candlestick Hammer. For simplicity, I went with the stricter definition disallowing an upper “shadow”. All this code is for demonstration-only, completely untested: Use at your own risk.
package com.basilbourque.example;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.Objects;
public class StockDay implements Comparable < StockDay > {
// Example data:
// Date Open High Low Close
// 31/10/2016 58.25 58.65 58.2 58.35
// ---------| Members |------------------------
private LocalDate date;
private BigDecimal open, high, low, close;
private Boolean isHammer;
// ---------| Constructors |------------------------
public StockDay ( final LocalDate localDateArg , final BigDecimal openArg , final BigDecimal highArg , final BigDecimal lowArg , final BigDecimal closeArg ) {
// In real work, add defensive code to validate data such as no nulls, only positive numbers, reasonable dates.
this.date = localDateArg;
this.open = openArg;
this.high = highArg;
this.low = lowArg;
// Verify the high is greater than or equal to the low.
if ( this.high.compareTo( this.low ) < 0 ) {
throw new IllegalArgumentException( "The passed High is below the passed Low for Date of " + this.date + ". Not possible." );
}
this.close = closeArg;
this.isHammer = this.determineHammer();
}
private Boolean determineHammer () {
// A hammer is a price pattern in candlestick charting that occurs when a security trades significantly lower than its opening, but rallies later in the day to close either above or near its opening price. This pattern forms a hammer-shaped candlestick, in which the body is at least half the size of the tail or wick.
// Read more: Hammer https://www.investopedia.com/terms/h/hammer.asp#ixzz5G6rqtbkv
// See also: http://www.onlinetradingconcepts.com/TechnicalAnalysis/Candlesticks/Hammer.html
// Caveat: This code is a quick rough draft, not thought-through, and totally untested. Use at your own risk. For demonstration purposes only.
// First check if the High is above the Close. A Hammer has little or no upper "shadow" (line protruding above the box). We'll go with "no shadow" for simplicity here.
if ( this.high.compareTo( this.close ) > 0 ) { // if high > close, not a hammer.
return Boolean.FALSE;
}
// Proceed with next check: Is "tail" (lower shadow) at least twice as long as height of box.
BigDecimal closeOpenDeltaAbsolute_BoxHeight = this.close.subtract( this.open ).abs();
BigDecimal lowerOfCloseOrOpen = ( this.close.compareTo( this.open ) < 0 ) ? this.close : this.open; // If x is smaller than y, use x. If x is greater than or equal to y, use y.
BigDecimal lowerShadowHeight = lowerOfCloseOrOpen.subtract( this.low );
// A Hammer has a long lower shadow (delta between either Close or Open, whichever is lower, and the Low), at least twice as tall as the box (Close-Open absolute delta).
BigDecimal requiredMinimumLengthFactorOfShadow = new BigDecimal( "2" );
BigDecimal doubleCloseOpenDeltaAbsolute = closeOpenDeltaAbsolute_BoxHeight.multiply( requiredMinimumLengthFactorOfShadow );
Boolean hammer = ( lowerShadowHeight.compareTo( doubleCloseOpenDeltaAbsolute ) > 0 );
return hammer;
}
// ---------| Accessors |------------------------
// All fields are read-only. Just getters, no setters.
public LocalDate getDate () {
return this.date;
}
public BigDecimal getOpen () {
return this.open;
}
public BigDecimal getHigh () {
return this.high;
}
public BigDecimal getLow () {
return this.low;
}
public BigDecimal getClose () {
return this.close;
}
public Boolean isHammer () {
return this.isHammer;
}
// ---------| Override `Object` |------------------------
@Override
public String toString () {
return "StockDay{ " +
"date=" + this.date +
", open=" + this.open +
", high=" + this.high +
", low=" + this.low +
", close=" + this.close +
", isHammer=" + this.isHammer +
" }";
}
@Override
public boolean equals ( final Object oArg ) {
if ( this == oArg ) return true;
if ( oArg == null || getClass() != oArg.getClass() ) return false;
final StockDay stockDay = ( StockDay ) oArg;
return Objects.equals( this.date , stockDay.date ) &&
Objects.equals( this.open , stockDay.open ) &&
Objects.equals( this.high , stockDay.high ) &&
Objects.equals( this.low , stockDay.low ) &&
Objects.equals( this.close , stockDay.close );
// Perhaps this should be coded to only consider the `LocalDate` field alone.
}
@Override
public int hashCode () {
return Objects.hash( this.date , this.open , this.high , this.low , this.close );
// Perhaps this should be coded to only consider the `LocalDate` field alone.
}
@Override
public int compareTo ( final StockDay o ) {
// Compare the date field only.
int result = this.getDate().compareTo( o.getDate() );
return result;
}
}
Define a class for loading data
Create a class to load your CSV data.
package com.basilbourque.example;
import java.io.Reader;
import java.io.StringReader;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.*;
public class StockDayLoader {
final private DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern( "dd/MM/uuuu" ); // Tip: Instead of this custom format, use standard ISO 8601 formats when exchanging date-time values as text.
public List < StockDay > loadFrom ( final Reader input ) {
final List < StockDay > stockDays = new ArrayList <>(); // To be populated by lines of data read from the input.
try ( // try-with-resources
Scanner scanner = new Scanner( input ).useDelimiter( "\r\n" ) ; // Delimiter is a comma between fields.
) {
scanner.useLocale( Locale.US ); // Determines cultural norms such as FULL STOP versus COMMA for decimal point in a `BigDecimal`.
// Skip first line, the column headers.
if ( scanner.hasNextLine() ) {
String headers = scanner.nextLine(); // Ignore returned String.
if ( ! "Date,Open,High,Low,Close".equals( headers ) ) { // Verify expected input.
throw new IllegalArgumentException( "The passed Readable object’s first row does not consist of expected column header names." );
}
}
while ( scanner.hasNextLine() ) {
String line = scanner.nextLine(); // Grab entire line.
try (
Scanner lineScanner = new Scanner( line ).useDelimiter( "," ).useLocale( Locale.US ) ;
) {
String dateInput = lineScanner.next();
LocalDate date = LocalDate.parse( dateInput , this.dateFormatter );
try {
BigDecimal open = lineScanner.nextBigDecimal();
BigDecimal high = lineScanner.nextBigDecimal();
BigDecimal low = lineScanner.nextBigDecimal();
BigDecimal close = lineScanner.nextBigDecimal();
StockDay stockDay = new StockDay( date , open , high , low , close );
stockDays.add( stockDay ); // Collect the newly intanstiated `StockDay` object.
} catch ( InputMismatchException e ) {
System.out.println( "ERROR The next token does not match the Decimal regular expression, or is out of range. " );
e.printStackTrace();
}
}
}
return stockDays;
}
}
// -----------| Testing/Demo |------------------------
public static String bogusData () {
final String eol = "\r\n"; // RFC 4180 requires CrLf as end-of-line.
final StringBuilder sb = new StringBuilder();
sb.append( "Date,Open,High,Low,Close" + eol );
sb.append( "31/10/2016,58.25,58.65,58.2,58.35" + eol );
sb.append( "28/10/2016,58.95,59,58.3,58.35" + eol );
sb.append( "27/10/2016,58.78,58.22,33.3,58.55" + eol ); // Hammer.
sb.append( "26/10/2016,58.95,59.05,58.43,58.45" + eol );
sb.append( "25/10/2016,58.99,58.44,22.2,58.57" + eol ); // Hammer.
String s = sb.toString();
return s;
}
public static void main ( String[] args ) {
String s = StockDayLoader.bogusData();
Reader reader = new StringReader( s );
StockDayLoader loader = new StockDayLoader();
List < StockDay > list = loader.loadFrom( reader );
System.out.println( list );
}
}
Apache Commons CSV
Even better, rather than write your own CSV reader, leverage existing well-used well-tested code found in a library such as Apache Commons CSV.
package com.basilbourque.example;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVRecord;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
public class StockDayLoaderEasy {
final private DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern( "dd/MM/uuuu" ); // Tip: Instead of this custom format, use standard ISO 8601 formats when exchanging date-time values as text.
public List < StockDay > loadFrom ( final Reader reader ) {
final List < StockDay > stockDays = new ArrayList <>(); // To be populated by lines of data read from the input.
Iterable < CSVRecord > records = null;
try {
records = CSVFormat.RFC4180.parse( reader );
} catch ( IOException eArg ) {
eArg.printStackTrace();
}
// Read each column. Names: "Date,Open,High,Low,Close"
for ( CSVRecord record : records ) {
LocalDate date = LocalDate.parse( record.get( "Date" ) , this.dateFormatter );
BigDecimal open = new BigDecimal( record.get( "Open" ) );
BigDecimal high = new BigDecimal( record.get( "High" ) );
BigDecimal low = new BigDecimal( record.get( "Low" ) );
BigDecimal close = new BigDecimal( record.get( "Close" ) );
StockDay stockDay = new StockDay( date , open , high , low , close );
stockDays.add( stockDay ); // Collect the newly intanstiated `StockDay` object.
}
return stockDays;
}
// -----------| Testing/Demo |------------------------
public static String bogusData () {
final String eol = "\r\n"; // RFC 4180 requires CrLf as end-of-line.
final StringBuilder sb = new StringBuilder();
sb.append( "Date,Open,High,Low,Close" + eol );
sb.append( "31/10/2016,58.25,58.65,58.2,58.35" + eol );
sb.append( "28/10/2016,58.95,59,58.3,58.35" + eol );
sb.append( "27/10/2016,58.78,58.22,33.3,58.55" + eol ); // Hammer.
sb.append( "26/10/2016,58.95,59.05,58.43,58.45" + eol );
sb.append( "25/10/2016,58.99,58.44,22.2,58.57" + eol ); // Hammer.
String s = sb.toString();
return s;
}
public static void main ( String[] args ) {
String s = StockDayLoader.bogusData();
Reader reader = new StringReader( s );
StockDayLoader loader = new StockDayLoader();
List < StockDay > list = loader.loadFrom( reader );
System.out.println( list );
}
}
Pull it all together
Create an app to exercise these pieces.
The goal of the Question was to find which stock-day-reports met the criteria of (a) date within a date-range, AND (b) is a Hammer.
Several short examples are presented here. Some use old-fashioned Java syntax, and some use modern Streams/Lambda syntax, with same results either way. Read the code-comments for guidance.
package com.basilbourque.example;
import org.threeten.extra.LocalDateRange;
import java.io.Reader;
import java.io.StringReader;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
public class HammerTime {
public static void main ( String[] args ) {
HammerTime hammerTime = new HammerTime();
hammerTime.doIt();
}
private void doIt () {
// Load all data.
Reader reader = new StringReader( StockDayLoader.bogusData() );
List < StockDay > stockDays = new StockDayLoader().loadFrom( reader );
Collections.sort( stockDays ); // Sort chronologically, ascending order, oldest first. For newest first, call `Collections.reverse`.
System.out.println( "All stockDays = " + stockDays );
// Find hammers using the old-fashioned way.
List < StockDay > hammers = new ArrayList <>();
for ( StockDay stockDay : stockDays ) {
if ( stockDay.isHammer() ) {
hammers.add( stockDay );
}
}
System.out.println( "hammers: " + hammers );
// Find hammers using modern Streams/Lambda features, while tolerating NULLs.
List < StockDay > hammers2 = stockDays.stream()
.filter( stockDay -> Objects.equals( stockDay.isHammer() , Boolean.TRUE ) ) // Use `Objects.equals` to tolerate NULL values.
.collect( Collectors.toList() );
System.out.println( "hammers2: " + hammers2 );
// Find hammers using modern Streams/Lambda features, assuming no NULL values exist.
List < StockDay > hammers3 = stockDays.stream()
.filter( stockDay -> stockDay.isHammer().equals( Boolean.TRUE ) ) // Simpler syntax than above, if you are certain of no NULLs.
.collect( Collectors.toList() );
System.out.println( "hammers3: " + hammers3 );
// Find hammers within a certain date-range, per the original Question.
// Parse the user’s input start/stop dates.
LocalDate start = LocalDate.parse( "2016-10-26" ); // This range should pick up the hammer on the 27th while omitting the hammer on the 25th.
LocalDate stop = LocalDate.parse( "2016-10-28" ); // Usual practice in defining a span-of-time is the Half-Open approach, where the beginning is *inclusive* while the ending is *exclusive*.
// Find hammers within date range using the old-fashioned syntax, with built-in classes.
List < StockDay > hammersInDateRange1 = new ArrayList <>();
for ( StockDay stockDay : stockDays ) {
if ( stockDay.isHammer() ) {
if ( ( ! stockDay.getDate().isBefore( start ) ) && stockDay.getDate().isBefore( stop ) ) {
hammersInDateRange1.add( stockDay );
}
}
}
System.out.println( "hammersInDateRange1: " + hammersInDateRange1 );
// Find hammers within date range using the old-fashioned syntax, with the ThreeTen-Extra library and its `LocalDateRange` class. http://www.threeten.org/threeten-extra/
final LocalDateRange dateRange = LocalDateRange.of( start , stop );
List < StockDay > hammersInDateRange2 = new ArrayList <>();
for ( StockDay stockDay : stockDays ) {
if ( stockDay.isHammer() ) {
if ( dateRange.contains( stockDay.getDate() ) ) {
hammersInDateRange2.add( stockDay );
}
}
}
System.out.println( "hammersInDateRange2: " + hammersInDateRange2 );
// Find hammers within date range using modern Streams/Lambda syntax, with the ThreeTen-Extra library and its `LocalDateRange` class. http://www.threeten.org/threeten-extra/
List < StockDay > hammersInDateRange3 = stockDays.stream()
.filter( stockDay -> stockDay.isHammer() && dateRange.contains( stockDay.getDate() ) ) // Assumes no NULLs.
.collect( Collectors.toList() );
System.out.println( "hammersInDateRange3: " + hammersInDateRange3 );
}
}
When run.
All stockDays = [StockDay{ date=2016-10-25, open=58.99, high=58.44, low=22.2, close=58.57, isHammer=true }, StockDay{ date=2016-10-26, open=58.95, high=59.05, low=58.43, close=58.45, isHammer=false }, StockDay{ date=2016-10-27, open=58.78, high=58.22, low=33.3, close=58.55, isHammer=true }, StockDay{ date=2016-10-28, open=58.95, high=59, low=58.3, close=58.35, isHammer=false }, StockDay{ date=2016-10-31, open=58.25, high=58.65, low=58.2, close=58.35, isHammer=false }]
hammers: [StockDay{ date=2016-10-25, open=58.99, high=58.44, low=22.2, close=58.57, isHammer=true }, StockDay{ date=2016-10-27, open=58.78, high=58.22, low=33.3, close=58.55, isHammer=true }]
hammers2: [StockDay{ date=2016-10-25, open=58.99, high=58.44, low=22.2, close=58.57, isHammer=true }, StockDay{ date=2016-10-27, open=58.78, high=58.22, low=33.3, close=58.55, isHammer=true }]
hammers3: [StockDay{ date=2016-10-25, open=58.99, high=58.44, low=22.2, close=58.57, isHammer=true }, StockDay{ date=2016-10-27, open=58.78, high=58.22, low=33.3, close=58.55, isHammer=true }]
hammersInDateRange1: [StockDay{ date=2016-10-27, open=58.78, high=58.22, low=33.3, close=58.55, isHammer=true }]
hammersInDateRange2: [StockDay{ date=2016-10-27, open=58.78, high=58.22, low=33.3, close=58.55, isHammer=true }]
hammersInDateRange3: [StockDay{ date=2016-10-27, open=58.78, high=58.22, low=33.3, close=58.55, isHammer=true }]
About java.time
The java.time framework is built into Java 8 and later. These classes supplant the troublesome old legacy date-time classes such as java.util.Date
, Calendar
, & SimpleDateFormat
.
The Joda-Time project, now in maintenance mode, advises migration to the java.time classes.
To learn more, see the Oracle Tutorial. And search Stack Overflow for many examples and explanations. Specification is JSR 310.
You may exchange java.time objects directly with your database. Use a JDBC driver compliant with JDBC 4.2 or later. No need for strings, no need for java.sql.*
classes.
Where to obtain the java.time classes?
The ThreeTen-Extra project extends java.time with additional classes. This project is a proving ground for possible future additions to java.time. You may find some useful classes here such as Interval
, YearWeek
, YearQuarter
, and more.